Merge pull request #559 from dezgeg/ps_refact

ps: Refactor process selection + support new flags
This commit is contained in:
Daniel Hofstetter
2025-11-03 14:10:02 +01:00
committed by GitHub
9 changed files with 254 additions and 153 deletions
+1 -1
View File
@@ -209,7 +209,7 @@ fn try_get_pattern_from(matches: &ArgMatches) -> UResult<String> {
pattern
};
Ok(pattern.to_string())
Ok(pattern.clone())
}
fn any_matches<T: Eq + Hash>(optional_ids: &Option<HashSet<T>>, id: T) -> bool {
-107
View File
@@ -1,107 +0,0 @@
// 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, rc::Rc};
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<i32> {
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<i32> {
Some(0)
}
// Default behavior: select processes with same terminal and same effective user ID
pub(crate) fn basic_collector(
proc_snapshot: &[Rc<RefCell<ProcessInformation>>],
) -> Vec<Rc<RefCell<ProcessInformation>>> {
let mut result = Vec::new();
let mut cur = ProcessInformation::current_process_info().unwrap();
let tty = cur.tty();
let euid = cur.euid().unwrap();
for proc_info in proc_snapshot {
if proc_info.borrow().tty() == tty && proc_info.borrow_mut().euid().unwrap() == euid {
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<RefCell<ProcessInformation>>],
) -> Vec<Rc<RefCell<ProcessInformation>>> {
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<RefCell<ProcessInformation>>],
) -> Vec<Rc<RefCell<ProcessInformation>>> {
let mut result = Vec::new();
let tty = |proc: &Rc<RefCell<ProcessInformation>>| proc.borrow_mut().tty();
// flag `-d`
if matches.get_flag("d") {
for proc_info in proc_snapshot {
let pid = proc_info.borrow().pid;
if getsid(pid as i32) != Some(pid as i32) {
result.push(proc_info.clone());
}
}
}
// flag `-a`
// Guessing it pid=sid, and associated terminal.
if matches.get_flag("a") {
for it in proc_snapshot {
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
}
+8 -1
View File
@@ -15,7 +15,7 @@ pub(crate) fn collect_code_mapping(formats: &[OptionalKeyValue]) -> Vec<(String,
let key = it.key().to_string();
match it.value() {
Some(value) => (key, value.clone()),
None => (key.clone(), mapping.get(&key).unwrap().to_string()),
None => (key.clone(), mapping.get(&key).unwrap().clone()),
}
})
.collect()
@@ -104,6 +104,13 @@ pub(crate) fn vm_format_codes() -> Vec<String> {
.to_vec()
}
/// Returns the format codes used when BSD flags (e.g. -x) are used.
pub(crate) fn bsd_format_codes() -> Vec<String> {
["pid", "tname", "stat", "time", "command"]
.map(Into::into)
.to_vec()
}
/// Returns the register format codes (for -X flag).
pub(crate) fn register_format_codes() -> Vec<String> {
[
+115
View File
@@ -0,0 +1,115 @@
// 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::collections::HashSet;
use uu_pgrep::process::{walk_process, ProcessInformation, RunState, Teletype};
use uucore::error::UResult;
#[cfg(target_family = "unix")]
use nix::errno::Errno;
// 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<i32> {
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<i32> {
Some(0)
}
fn is_session_leader(process: &ProcessInformation) -> bool {
let pid = process.pid as i32;
getsid(pid) == Some(pid)
}
pub struct ProcessSelectionSettings {
/// - `-A` Select all processes. Identical to `-e`.
pub select_all: bool,
/// - `-a` Select all processes except both session leaders (see getsid(2)) and processes not associated with a terminal.
pub select_non_session_leaders_with_tty: bool,
/// - `-d` Select all processes except session leaders.
pub select_non_session_leaders: bool,
/// - '-x' Lift "must have a tty" restriction.
pub dont_require_tty: bool,
/// Select specific process IDs (-p, --pid)
pub pids: Option<HashSet<usize>>,
/// - `-r` Restrict the selection to only running processes.
pub only_running: bool,
/// - `--deselect` Negates the selection.
pub negate_selection: bool,
}
impl ProcessSelectionSettings {
pub fn from_matches(matches: &ArgMatches) -> Self {
Self {
select_all: matches.get_flag("A"),
select_non_session_leaders_with_tty: matches.get_flag("a"),
select_non_session_leaders: matches.get_flag("d"),
dont_require_tty: matches.get_flag("x"),
pids: matches
.get_many::<Vec<usize>>("pid")
.map(|xs| xs.flatten().copied().collect()),
only_running: matches.get_flag("r"),
negate_selection: matches.get_flag("deselect"),
}
}
pub fn select_processes(self) -> UResult<Vec<ProcessInformation>> {
let mut current_process = ProcessInformation::current_process_info().unwrap();
let current_tty = current_process.tty();
let current_euid = current_process.euid().unwrap();
let matches_criteria = |process: &mut ProcessInformation| -> UResult<bool> {
if self.only_running && !process.run_state().is_ok_and(|x| x == RunState::Running) {
return Ok(false);
}
if let Some(ref pids) = self.pids {
return Ok(pids.contains(&process.pid));
}
if self.select_all {
return Ok(true);
}
if self.select_non_session_leaders_with_tty {
return Ok(!is_session_leader(process) && process.tty() != Teletype::Unknown);
}
if self.select_non_session_leaders {
return Ok(!is_session_leader(process));
}
// Default behavior: select processes with same effective user ID and same tty (except -x removes tty restriction)
Ok(process.euid().unwrap() == current_euid
&& (self.dont_require_tty || process.tty() == current_tty))
};
let mut selected = vec![];
for mut process in walk_process() {
if matches_criteria(&mut process)? ^ self.negate_selection {
selected.push(process);
}
}
Ok(selected)
}
}
+53 -38
View File
@@ -3,24 +3,24 @@
// 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 process_selection;
mod sorting;
use clap::crate_version;
use clap::{Arg, ArgAction, ArgMatches, Command};
use mapping::{
collect_code_mapping, default_codes, default_mapping, default_with_psr_codes,
bsd_format_codes, collect_code_mapping, default_codes, default_mapping, default_with_psr_codes,
extra_full_format_codes, full_format_codes, job_format_codes, long_format_codes,
long_y_format_codes, register_format_codes, signal_format_codes, user_format_codes,
vm_format_codes,
};
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 process_selection::ProcessSelectionSettings;
use std::cell::RefCell;
use uucore::{
error::{UError, UResult, USimpleError},
format_usage, help_about, help_usage,
@@ -33,21 +33,12 @@ const USAGE: &str = help_usage!("ps.md");
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::<Vec<_>>();
let mut proc_infos = Vec::new();
if !matches.get_flag("A") && !matches.get_flag("a") && !matches.get_flag("d") {
proc_infos.extend(collector::basic_collector(&snapshot));
} else {
proc_infos.extend(collector::process_collector(&matches, &snapshot));
proc_infos.extend(collector::session_collector(&matches, &snapshot));
let selection_settings = ProcessSelectionSettings::from_matches(&matches);
let mut proc_infos = selection_settings.select_processes()?;
if proc_infos.is_empty() {
uucore::error::set_exit_code(1);
}
proc_infos.sort_by(|a, b| a.borrow().pid.cmp(&b.borrow().pid));
proc_infos.dedup_by(|a, b| a.borrow().pid == b.borrow().pid);
sorting::sort(&mut proc_infos, &matches);
let arg_formats = collect_format(&matches);
@@ -74,6 +65,8 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
user_format_codes()
} else if matches.get_flag("v") {
vm_format_codes()
} else if matches.get_flag("x") {
bsd_format_codes()
} else if matches.get_flag("X") {
register_format_codes()
} else if arg_formats.is_empty() {
@@ -90,7 +83,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
for proc in proc_infos {
let picked = pickers
.iter()
.map(|picker| picker(Rc::unwrap_or_clone(proc.clone())));
.map(|picker| picker(RefCell::new(proc.clone())));
rows.push(Row::from_iter(picked));
}
@@ -100,21 +93,22 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
default_codes();
codes
.into_iter()
.map(|code| (code.clone(), default_mapping[&code].to_string()))
.map(|code| (code.clone(), default_mapping[&code].clone()))
.collect::<Vec<_>>()
} else {
collect_code_mapping(&arg_formats)
};
let header = code_mapping
.iter()
.map(|(_, header)| header)
.map(Into::into)
.collect::<Vec<String>>();
// Apply header
let mut table = Table::from_iter([Row::from_iter(header)]);
let mut table = Table::new();
table.set_format(*FORMAT_CLEAN);
if !matches.get_flag("no-headers") {
let header = code_mapping
.iter()
.map(|(_, header)| header)
.map(Into::into)
.collect::<Vec<String>>();
table.add_row(Row::from_iter(header));
}
table.extend(rows);
print!("{table}");
@@ -144,6 +138,15 @@ fn collect_format(
Ok(collect)
}
fn parse_numeric_list(s: &str) -> Result<Vec<usize>, String> {
s.split(|c: char| c.is_whitespace() || c == ',')
.map(|word| {
word.parse::<usize>()
.map_err(|_| format!("invalid number: '{}'", word))
})
.collect()
}
#[allow(clippy::cognitive_complexity)]
pub fn uu_app() -> Command {
Command::new(uucore::util_name())
@@ -177,21 +180,19 @@ pub fn uu_app() -> Command {
.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("r")
.short('r')
.action(ArgAction::SetTrue)
.help("only running processes"),
// 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::new("x")
.short('x')
.action(ArgAction::SetTrue)
.help("processes without controlling ttys"),
])
.arg(
Arg::new("f")
@@ -263,6 +264,21 @@ pub fn uu_app() -> Command {
.value_parser(parser)
.help("user-defined format"),
)
.arg(
Arg::new("no-headers")
.long("no-headers")
.visible_alias("no-heading")
.action(ArgAction::SetTrue)
.help("do not print header at all"),
)
.arg(
Arg::new("pid")
.short('p')
.long("pid")
.action(ArgAction::Append)
.value_parser(parse_numeric_list)
.help("select by process ID"),
)
// .args([
// Arg::new("command").short('c').help("command name"),
// Arg::new("GID")
@@ -273,7 +289,6 @@ pub fn uu_app() -> Command {
// .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')
+3 -4
View File
@@ -4,15 +4,14 @@
// 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<RefCell<ProcessInformation>>], _matches: &ArgMatches) {
pub(crate) fn sort(input: &mut [ProcessInformation], _matches: &ArgMatches) {
sort_by_pid(input);
}
/// Sort by pid. (Default)
fn sort_by_pid(input: &mut [Rc<RefCell<ProcessInformation>>]) {
input.sort_by(|a, b| a.borrow().pid.cmp(&b.borrow().pid));
fn sort_by_pid(input: &mut [ProcessInformation]) {
input.sort_by(|a, b| a.pid.cmp(&b.pid));
}
+1 -1
View File
@@ -293,7 +293,7 @@ pub(crate) fn parse_data(line: &str) -> Option<(String, Vec<u64>)> {
split.first().map(|name| {
(
name.to_string(),
name.clone(),
split
.clone()
.into_iter()
+1 -1
View File
@@ -324,7 +324,7 @@ fn get_process_info(
let blocked = proc_data.stat.get("procs_blocked").unwrap();
let len = if matches.get_flag("wide") { 4 } else { 2 };
vec![(len, runnable.to_string()), (len, blocked.to_string())]
vec![(len, runnable.clone()), (len, blocked.clone())]
}
#[cfg(target_os = "linux")]
+72
View File
@@ -3,6 +3,8 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
#[cfg(target_os = "linux")]
use regex::Regex;
use uutests::new_ucmd;
#[test]
@@ -129,6 +131,12 @@ fn test_register_format() {
);
}
#[test]
#[cfg(target_os = "linux")]
fn test_x_format() {
check_header("-x", &["PID", "TTY", "STAT", "TIME", "COMMAND"]);
}
#[test]
#[cfg(target_os = "linux")]
fn test_code_mapping() {
@@ -165,3 +173,67 @@ fn test_code_mapping() {
.stdout_contains("CMD1")
.stdout_contains("CMD2");
}
#[test]
#[cfg(target_os = "linux")]
fn test_no_headers_flags() {
let regex = Regex::new("^ *PID +").unwrap();
for flag in &["--no-headers", "--no-heading"] {
new_ucmd!()
.arg(flag)
.succeeds()
.stdout_does_not_match(&regex);
}
}
#[test]
#[cfg(target_os = "linux")]
fn test_deselect() {
// Inverse of all processes should be empty
new_ucmd!()
.args(&["--deselect", "-A", "--no-headers"])
.fails()
.code_is(1)
.stdout_is("");
// PID 1 should be present in inverse of default filter criteria
new_ucmd!()
.args(&["--deselect"])
.succeeds()
.stdout_matches(&Regex::new("\n *1 ").unwrap());
}
#[test]
#[cfg(target_os = "linux")]
fn test_pid_selection() {
let our_pid = std::process::id();
// Test that only pid 1 and pid of the test runner is present
let test = |pid_args: &[&str]| {
let match_regex = Regex::new(&format!("^ *1 *\n *{our_pid} *\n$")).unwrap();
let mut args = vec!["--no-headers", "-o", "pid"];
args.extend_from_slice(pid_args);
new_ucmd!()
.args(&args)
.succeeds()
.stdout_matches(&match_regex);
};
for flag in ["-p", "--pid"] {
test(&[flag, &format!("1 {our_pid}")]);
test(&[flag, &format!("1,{our_pid}")]);
test(&[flag, "1", flag, &our_pid.to_string()]);
}
// Test nonexistent PID (should show no output)
new_ucmd!()
.args(&["-p", "0", "--no-headers"])
.fails()
.code_is(1)
.stdout_is("");
// Test invalid PID
new_ucmd!()
.args(&["-p", "invalid"])
.fails()
.stderr_contains("invalid number");
}