From 95d97fa2f3bf9b662865971c2697fcc582affc29 Mon Sep 17 00:00:00 2001 From: Bluemangoo Date: Mon, 18 Aug 2025 12:46:48 +0800 Subject: [PATCH] pgrep&pidwait&snice&skill: implement `ns` `nslist` --- src/uu/pgrep/src/process.rs | 118 ++++++++++++++++++++++++++++ src/uu/pgrep/src/process_matcher.rs | 45 +++++++++-- 2 files changed, 158 insertions(+), 5 deletions(-) diff --git a/src/uu/pgrep/src/process.rs b/src/uu/pgrep/src/process.rs index a229d16b..724cc4de 100644 --- a/src/uu/pgrep/src/process.rs +++ b/src/uu/pgrep/src/process.rs @@ -218,6 +218,106 @@ impl TryFrom<&str> for CgroupMembership { } } +// See https://www.man7.org/linux/man-pages/man7/namespaces.7.html +#[derive(Default)] +pub struct Namespace { + pub ipc: Option, + pub mnt: Option, + pub net: Option, + pub pid: Option, + pub user: Option, + pub uts: Option, +} + +impl Namespace { + pub fn new() -> Self { + Namespace { + ipc: None, + mnt: None, + net: None, + pid: None, + user: None, + uts: None, + } + } + + pub fn from_pid(pid: usize) -> Result { + let mut ns = Namespace::new(); + let path = PathBuf::from(format!("/proc/{pid}/ns")); + for entry in fs::read_dir(path)? { + let entry = entry?; + if let Some(name) = entry.file_name().to_str() { + if let Ok(value) = read_link(entry.path()) { + match name { + "ipc" => ns.ipc = Some(value.to_str().unwrap_or_default().to_string()), + "mnt" => ns.mnt = Some(value.to_str().unwrap_or_default().to_string()), + "net" => ns.net = Some(value.to_str().unwrap_or_default().to_string()), + "pid" => ns.pid = Some(value.to_str().unwrap_or_default().to_string()), + "user" => ns.user = Some(value.to_str().unwrap_or_default().to_string()), + "uts" => ns.uts = Some(value.to_str().unwrap_or_default().to_string()), + _ => {} + } + } + } + } + Ok(ns) + } + + pub fn filter(&mut self, filters: &[&str]) { + if !filters.contains(&"ipc") { + self.ipc = None; + } + if !filters.contains(&"mnt") { + self.mnt = None; + } + if !filters.contains(&"net") { + self.net = None; + } + if !filters.contains(&"pid") { + self.pid = None; + } + if !filters.contains(&"user") { + self.user = None; + } + if !filters.contains(&"uts") { + self.uts = None; + } + } + + pub fn matches(&self, ns: &Namespace) -> bool { + ns.ipc.is_some() + && self + .ipc + .as_ref() + .is_some_and(|v| v == ns.ipc.as_ref().unwrap()) + || ns.mnt.is_some() + && self + .mnt + .as_ref() + .is_some_and(|v| v == ns.mnt.as_ref().unwrap()) + || ns.net.is_some() + && self + .net + .as_ref() + .is_some_and(|v| v == ns.net.as_ref().unwrap()) + || ns.pid.is_some() + && self + .pid + .as_ref() + .is_some_and(|v| v == ns.pid.as_ref().unwrap()) + || ns.user.is_some() + && self + .user + .as_ref() + .is_some_and(|v| v == ns.user.as_ref().unwrap()) + || ns.uts.is_some() + && self + .uts + .as_ref() + .is_some_and(|v| v == ns.uts.as_ref().unwrap()) + } +} + /// Process ID and its information #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct ProcessInformation { @@ -552,6 +652,10 @@ impl ProcessInformation { Ok(env_vars) } + + pub fn namespaces(&self) -> Result { + Namespace::from_pid(self.pid) + } } impl TryFrom for ProcessInformation { type Error = io::Error; @@ -754,6 +858,20 @@ mod tests { } } + #[test] + #[cfg(target_os = "linux")] + fn test_namespaces() { + let pid_entry = ProcessInformation::current_process_info().unwrap(); + let namespaces = pid_entry.namespaces().unwrap(); + + assert!(namespaces.ipc.is_some()); + assert!(namespaces.mnt.is_some()); + assert!(namespaces.net.is_some()); + assert!(namespaces.pid.is_some()); + assert!(namespaces.user.is_some()); + assert!(namespaces.uts.is_some()); + } + #[test] #[cfg(target_os = "linux")] fn test_environ() { diff --git a/src/uu/pgrep/src/process_matcher.rs b/src/uu/pgrep/src/process_matcher.rs index 1a14cd44..40936699 100644 --- a/src/uu/pgrep/src/process_matcher.rs +++ b/src/uu/pgrep/src/process_matcher.rs @@ -23,7 +23,7 @@ use uucore::{ use uucore::error::{UResult, USimpleError}; -use crate::process::{walk_process, walk_threads, ProcessInformation, Teletype}; +use crate::process::{walk_process, walk_threads, Namespace, ProcessInformation, Teletype}; pub struct Settings { pub regex: Regex, @@ -47,6 +47,7 @@ pub struct Settings { pub pgroup: Option>, pub session: Option>, pub cgroup: Option>, + pub namespaces: Option, pub env: Option>, pub threads: bool, @@ -112,6 +113,17 @@ pub fn get_match_settings(matches: &ArgMatches) -> UResult { cgroup: matches .get_many::("cgroup") .map(|groups| groups.cloned().collect()), + namespaces: matches + .get_one::("ns") + .map(|pid| { + get_namespaces( + *pid, + matches + .get_many::("nslist") + .map(|v| v.into_iter().map(|s| s.as_str()).collect()), + ) + }) + .transpose()?, env: matches .get_many::("env") .map(|env_vars| env_vars.cloned().collect()), @@ -133,6 +145,7 @@ pub fn get_match_settings(matches: &ArgMatches) -> UResult { && settings.pgroup.is_none() && settings.session.is_none() && settings.cgroup.is_none() + && settings.namespaces.is_none() && settings.env.is_none() && !settings.require_handler && settings.pidfile.is_none() @@ -216,6 +229,22 @@ fn get_ancestors(process_infos: &mut [ProcessInformation], mut pid: usize) -> Ha ret } +#[cfg(target_os = "linux")] +fn get_namespaces(pid: usize, list: Option>) -> UResult { + let mut ns = Namespace::from_pid(pid) + .map_err(|_| USimpleError::new(1, "Error reading reference namespace information"))?; + if let Some(list) = list { + ns.filter(&list); + } + + Ok(ns) +} + +#[cfg(not(target_os = "linux"))] +fn get_namespaces(_pid: usize, _list: Option>) -> UResult { + Ok(Namespace::new()) +} + /// Collect pids with filter construct from command line arguments fn collect_matched_pids(settings: &Settings) -> UResult> { // Filtration general parameters @@ -283,6 +312,10 @@ fn collect_matched_pids(settings: &Settings) -> UResult> &settings.cgroup, pid.cgroup_v2_path().unwrap_or("/".to_string()), ); + let namespace_matched = settings + .namespaces + .as_ref() + .is_none_or(|ns| ns.matches(&pid.namespaces().unwrap_or_default())); let env_matched = match &settings.env { Some(env_filters) => { @@ -331,6 +364,7 @@ fn collect_matched_pids(settings: &Settings) -> UResult> && pgroup_matched && session_matched && cgroup_matched + && namespace_matched && env_matched && ids_matched && handler_matched @@ -552,10 +586,11 @@ pub fn clap_args(pattern_help: &'static str, enable_v_flag: bool) -> Vec { arg!(-A --"ignore-ancestors" "exclude our ancestors from results"), arg!(--cgroup "match by cgroup v2 names").value_delimiter(','), arg!(--env <"name[=val],..."> "match on environment variable").value_delimiter(','), - // arg!(--ns "match the processes that belong to the same namespace as "), - // arg!(--nslist "list which namespaces will be considered for the --ns option.") - // .value_delimiter(',') - // .value_parser(["ipc", "mnt", "net", "pid", "user", "uts"]), + arg!(--ns "match the processes that belong to the same namespace as ") + .value_parser(clap::value_parser!(usize)), + arg!(--nslist "list which namespaces will be considered for the --ns option.") + .value_delimiter(',') + .value_parser(["ipc", "mnt", "net", "pid", "user", "uts"]), Arg::new("pattern") .help(pattern_help) .action(ArgAction::Append)