cgroups_explorer/
explorer.rs

1use std::{
2    collections::{HashSet, hash_set},
3    path::{Path, PathBuf},
4};
5
6use cgroups_rs::{
7    Cgroup, Hierarchy,
8    hierarchies::{V1, V2, is_cgroup2_unified_mode},
9};
10use derive_builder::Builder;
11use walkdir::WalkDir;
12
13/// An interface to explore cgroups in the system.
14///
15/// # Example
16/// ```rust
17/// use cgroups_explorer::Explorer;
18/// let explorer = Explorer::detect_version()
19///     .include(vec!["user.slice/*".to_string()])
20///     .build()
21///     .expect("Failed to build explorer");
22/// let found = explorer
23///     .iter_cgroups()
24///     .for_each(|c| println!("Found cgroup: {}", c.path()));
25///
26/// ```
27#[derive(Builder)]
28#[builder(pattern = "owned")]
29pub struct Explorer {
30    /// The cgroup hierarchy to explore.
31    hierarchy: Box<dyn Hierarchy>,
32
33    /// The globs to include in the exploration.
34    #[builder(field(ty = "Vec<String>", build = "parse_include(self.include)?"))]
35    include: Vec<glob::Pattern>,
36    /// The regexes to match group names against.
37    #[cfg_attr(
38        feature = "regex",
39        builder(field(ty = "Vec<String>", build = "parse_include_regex(self.include_regex)?"))
40    )]
41    #[cfg(feature = "regex")]
42    include_regex: Vec<regex::Regex>,
43}
44
45/// An iterator over cgroups in the system that match the globs.
46struct CgroupsV2Iterator {
47    walker: walkdir::IntoIter,
48    include: Vec<glob::Pattern>,
49    #[cfg(feature = "regex")]
50    include_regex: Vec<regex::Regex>,
51    base_path: PathBuf,
52}
53
54struct CgroupsV1Iterator {
55    discovered: hash_set::IntoIter<PathBuf>,
56}
57
58impl Explorer {
59    /// Create a new `ExplorerBuilder` for cgroups v1.
60    #[must_use]
61    pub fn v1() -> ExplorerBuilder {
62        ExplorerBuilder::default().hierarchy(Box::new(V1::new()))
63    }
64
65    /// Create a new `ExplorerBuilder` for cgroups v2.
66    #[must_use]
67    pub fn v2() -> ExplorerBuilder {
68        ExplorerBuilder::default().hierarchy(Box::new(V2::new()))
69    }
70
71    /// Create a new `ExplorerBuilder` by detecting the cgroups version on the system.
72    #[must_use]
73    pub fn detect_version() -> ExplorerBuilder {
74        if is_cgroup2_unified_mode() {
75            ExplorerBuilder::default().hierarchy(Box::new(V2::new()))
76        } else {
77            ExplorerBuilder::default().hierarchy(Box::new(V1::new()))
78        }
79    }
80
81    /// Create an iterator over all cgroups in the system, based on the criteria.
82    #[must_use]
83    pub fn iter_cgroups(&self) -> Box<dyn Iterator<Item = Cgroup>> {
84        if self.hierarchy.v2() {
85            Box::new(self.iter_cgroups_v2())
86        } else {
87            Box::new(self.iter_cgroups_v1())
88        }
89    }
90
91    fn iter_cgroups_v2(&self) -> CgroupsV2Iterator {
92        let base_path = self.hierarchy.root();
93        let walker = WalkDir::new(base_path.clone())
94            .min_depth(1)
95            .sort_by_file_name()
96            .into_iter();
97        CgroupsV2Iterator {
98            walker,
99            include: self.include.clone(),
100            #[cfg(feature = "regex")]
101            include_regex: self.include_regex.clone(),
102            base_path,
103        }
104    }
105
106    fn iter_cgroups_v1(&self) -> CgroupsV1Iterator {
107        let hierarchy = V1::new();
108        let subystems = hierarchy.subsystems();
109        let base_path = hierarchy.root();
110
111        let mut matching_rel_paths = HashSet::new();
112        for subsystem in subystems {
113            let name = subsystem.controller_name();
114            let walker = WalkDir::new(base_path.join(&name))
115                .min_depth(1)
116                .sort_by_file_name()
117                .into_iter();
118            let base_controller_path = base_path.join(name);
119            for entry in walker {
120                let Ok(entry) = entry else { continue };
121                let path = entry.path();
122                if !entry.file_type().is_dir() {
123                    continue;
124                }
125                let Ok(relative_path) = path.strip_prefix(&base_controller_path) else {
126                    continue;
127                };
128                if relative_path.components().count() == 0 {
129                    continue;
130                }
131                #[cfg(feature = "regex")]
132                let should_include = path_matches_include(&self.include, relative_path)
133                    || path_matches_include_regex(&self.include_regex, relative_path);
134                #[cfg(not(feature = "regex"))]
135                let should_include = path_matches_include(&self.include, relative_path);
136
137                if should_include {
138                    matching_rel_paths.insert(relative_path.to_path_buf());
139                }
140            }
141        }
142
143        CgroupsV1Iterator {
144            discovered: matching_rel_paths.into_iter(),
145        }
146    }
147}
148
149impl Iterator for CgroupsV2Iterator {
150    type Item = Cgroup;
151
152    fn next(&mut self) -> Option<Self::Item> {
153        loop {
154            let entry = self.walker.next();
155            match entry {
156                Some(Ok(entry)) => {
157                    let path = entry.path();
158                    if !entry.file_type().is_dir() {
159                        continue;
160                    }
161                    let Ok(relative_path) = path.strip_prefix(&self.base_path) else {
162                        continue;
163                    };
164                    if relative_path.components().count() == 0 {
165                        continue;
166                    }
167                    if !path_matches_include(&self.include, relative_path) {
168                        continue;
169                    }
170                    #[cfg(feature = "regex")]
171                    if !path_matches_include_regex(&self.include_regex, relative_path) {
172                        continue;
173                    }
174                    return Some(Cgroup::load(Box::new(V2::new()), relative_path));
175                }
176                Some(Err(_e)) => return None,
177                None => return None,
178            }
179        }
180    }
181}
182
183impl Iterator for CgroupsV1Iterator {
184    type Item = Cgroup;
185
186    fn next(&mut self) -> Option<Self::Item> {
187        self.discovered
188            .next()
189            .map(|path| Cgroup::load(Box::new(V1::new()), path))
190    }
191}
192
193fn path_matches_include(include: &[glob::Pattern], path: &Path) -> bool {
194    if include.is_empty() {
195        return true;
196    }
197    let path_str = path.to_string_lossy();
198    include.iter().any(|pattern| pattern.matches(&path_str))
199}
200
201#[cfg(feature = "regex")]
202fn path_matches_include_regex(include: &[regex::Regex], path: &Path) -> bool {
203    if include.is_empty() {
204        return true;
205    }
206    let path_str = path.to_string_lossy();
207    include.iter().any(|pattern| pattern.is_match(&path_str))
208}
209
210fn parse_include(include: Vec<String>) -> Result<Vec<glob::Pattern>, ExplorerBuilderError> {
211    if include.is_empty() {
212        Ok(Vec::new())
213    } else {
214        include
215            .into_iter()
216            .map(|include| {
217                glob::Pattern::new(&include)
218                    .map_err(|e| ExplorerBuilderError::ValidationError(e.to_string()))
219            })
220            .collect()
221    }
222}
223
224#[cfg(feature = "regex")]
225fn parse_include_regex(include: Vec<String>) -> Result<Vec<regex::Regex>, ExplorerBuilderError> {
226    if include.is_empty() {
227        Ok(Vec::new())
228    } else {
229        include
230            .into_iter()
231            .map(|include| {
232                regex::Regex::new(&include)
233                    .map_err(|e| ExplorerBuilderError::ValidationError(e.to_string()))
234            })
235            .collect()
236    }
237}