Skip to content

Commit 23ad5d8

Browse files
authored
Merge pull request #413 from dezgeg/cgroup
pgrep/pkill/pidwait: Support --cgroup option
2 parents 0d79c68 + 835a930 commit 23ad5d8

File tree

3 files changed

+101
-2
lines changed

3 files changed

+101
-2
lines changed

src/uu/pgrep/src/process.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,37 @@ impl TryFrom<&String> for RunState {
187187
}
188188
}
189189

190+
/// Represents an entry in `/proc/<pid>/cgroup`.
191+
#[derive(Debug, Clone, PartialEq, Eq)]
192+
pub struct CgroupMembership {
193+
pub hierarchy_id: u32,
194+
pub controllers: Vec<String>,
195+
pub cgroup_path: String,
196+
}
197+
198+
impl TryFrom<&str> for CgroupMembership {
199+
type Error = io::Error;
200+
201+
fn try_from(value: &str) -> Result<Self, Self::Error> {
202+
let parts: Vec<&str> = value.split(':').collect();
203+
if parts.len() != 3 {
204+
return Err(io::ErrorKind::InvalidData.into());
205+
}
206+
207+
Ok(CgroupMembership {
208+
hierarchy_id: parts[0]
209+
.parse::<u32>()
210+
.map_err(|_| io::ErrorKind::InvalidData)?,
211+
controllers: if parts[1].is_empty() {
212+
vec![]
213+
} else {
214+
parts[1].split(',').map(String::from).collect()
215+
},
216+
cgroup_path: parts[2].to_string(),
217+
})
218+
}
219+
}
220+
190221
/// Process ID and its information
191222
#[derive(Debug, Clone, Default, PartialEq, Eq)]
192223
pub struct ProcessInformation {
@@ -383,6 +414,24 @@ impl ProcessInformation {
383414
read_link(format!("/proc/{}/root", self.pid))
384415
}
385416

417+
/// Returns cgroups (both v1 and v2) that the process belongs to.
418+
pub fn cgroups(&mut self) -> Result<Vec<CgroupMembership>, io::Error> {
419+
fs::read_to_string(format!("/proc/{}/cgroup", self.pid))?
420+
.lines()
421+
.map(CgroupMembership::try_from)
422+
.collect()
423+
}
424+
425+
/// Returns path to the v2 cgroup that the process belongs to.
426+
pub fn cgroup_v2_path(&mut self) -> Result<String, io::Error> {
427+
const V2_HIERARCHY_ID: u32 = 0;
428+
self.cgroups()?
429+
.iter()
430+
.find(|cg| cg.hierarchy_id == V2_HIERARCHY_ID)
431+
.map(|cg| cg.cgroup_path.clone())
432+
.ok_or(io::ErrorKind::NotFound.into())
433+
}
434+
386435
/// Fetch run state from [ProcessInformation::cached_stat]
387436
///
388437
/// - [The /proc Filesystem: Table 1-4](https://docs.kernel.org/filesystems/proc.html#id10)
@@ -625,4 +674,22 @@ mod tests {
625674
let mut pid_entry = ProcessInformation::current_process_info().unwrap();
626675
assert_eq!(pid_entry.root().unwrap(), PathBuf::from("/"));
627676
}
677+
678+
#[test]
679+
#[cfg(target_os = "linux")]
680+
fn test_cgroups() {
681+
let mut pid_entry = ProcessInformation::try_new("/proc/1".into()).unwrap();
682+
if pid_entry.name().unwrap() == "systemd" {
683+
let cgroups = pid_entry.cgroups().unwrap();
684+
if let Some(membership) = cgroups.iter().find(|cg| cg.hierarchy_id == 0) {
685+
let expected = CgroupMembership {
686+
hierarchy_id: 0,
687+
controllers: vec![],
688+
cgroup_path: "/init.scope".to_string(),
689+
};
690+
assert_eq!(membership, &expected);
691+
assert_eq!(pid_entry.cgroup_v2_path().unwrap(), "/init.scope");
692+
}
693+
}
694+
}
628695
}

src/uu/pgrep/src/process_matcher.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ pub struct Settings {
4444
pub gid: Option<HashSet<u32>>,
4545
pub pgroup: Option<HashSet<u64>>,
4646
pub session: Option<HashSet<u64>>,
47+
pub cgroup: Option<HashSet<String>>,
4748
pub threads: bool,
4849
}
4950

@@ -101,6 +102,9 @@ pub fn get_match_settings(matches: &ArgMatches) -> UResult<Settings> {
101102
})
102103
.collect()
103104
}),
105+
cgroup: matches
106+
.get_many::<String>("cgroup")
107+
.map(|groups| groups.cloned().collect()),
104108
threads: false,
105109
};
106110

@@ -115,6 +119,7 @@ pub fn get_match_settings(matches: &ArgMatches) -> UResult<Settings> {
115119
&& settings.gid.is_none()
116120
&& settings.pgroup.is_none()
117121
&& settings.session.is_none()
122+
&& settings.cgroup.is_none()
118123
&& !settings.require_handler
119124
&& pattern.is_empty()
120125
{
@@ -235,6 +240,10 @@ fn collect_matched_pids(settings: &Settings) -> Vec<ProcessInformation> {
235240
let parent_matched = any_matches(&settings.parent, pid.ppid().unwrap());
236241
let pgroup_matched = any_matches(&settings.pgroup, pid.pgid().unwrap());
237242
let session_matched = any_matches(&settings.session, pid.sid().unwrap());
243+
let cgroup_matched = any_matches(
244+
&settings.cgroup,
245+
pid.cgroup_v2_path().unwrap_or("/".to_string()),
246+
);
238247

239248
let ids_matched = any_matches(&settings.uid, pid.uid().unwrap())
240249
&& any_matches(&settings.euid, pid.euid().unwrap())
@@ -265,6 +274,7 @@ fn collect_matched_pids(settings: &Settings) -> Vec<ProcessInformation> {
265274
&& parent_matched
266275
&& pgroup_matched
267276
&& session_matched
277+
&& cgroup_matched
268278
&& ids_matched
269279
&& handler_matched)
270280
^ settings.inverse
@@ -413,8 +423,7 @@ pub fn clap_args(pattern_help: &'static str, enable_v_flag: bool) -> Vec<Arg> {
413423
// arg!(-L --logpidfile "fail if PID file is not locked"),
414424
arg!(-r --runstates <state> "match runstates [D,S,Z,...]"),
415425
// arg!(-A --"ignore-ancestors" "exclude our ancestors from results"),
416-
// arg!(--cgroup <grp> "match by cgroup v2 names")
417-
// .value_delimiter(','),
426+
arg!(--cgroup <grp> "match by cgroup v2 names").value_delimiter(','),
418427
// arg!(--ns <PID> "match the processes that belong to the same namespace as <pid>"),
419428
// arg!(--nslist <ns> "list which namespaces will be considered for the --ns option.")
420429
// .value_delimiter(',')

tests/by-util/test_pgrep.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -501,6 +501,29 @@ fn test_nonexisting_session() {
501501
new_ucmd!().arg("--session=9999999999").fails();
502502
}
503503

504+
#[test]
505+
#[cfg(target_os = "linux")]
506+
fn test_nonexisting_cgroup() {
507+
new_ucmd!()
508+
.arg("--cgroup")
509+
.arg("NONEXISTING")
510+
.fails()
511+
.stdout_is("");
512+
}
513+
514+
#[test]
515+
#[cfg(target_os = "linux")]
516+
fn test_cgroup() {
517+
let init_is_systemd = new_ucmd!().arg("systemd").run().code() == 0;
518+
if init_is_systemd {
519+
new_ucmd!()
520+
.arg("--cgroup")
521+
.arg("/init.scope")
522+
.succeeds()
523+
.stdout_is("1\n");
524+
}
525+
}
526+
504527
#[test]
505528
#[cfg(target_os = "linux")]
506529
fn test_threads() {

0 commit comments

Comments
 (0)