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(default, try_setter, setter(custom, name = "include_regex_str"))
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 ExplorerBuilder {
150    /// The regexes to match group names against.
151    ///
152    /// # Errors
153    /// If any of the regexes are invalid, a `ValidationError` is returned.
154    #[cfg(feature = "regex")]
155    pub fn include_regex_str<S: AsRef<str>>(
156        self,
157        include: &[S],
158    ) -> Result<Self, ExplorerBuilderError> {
159        let include_regex = parse_include_regex(include)
160            .map_err(|e| ExplorerBuilderError::ValidationError(e.to_string()))?;
161        Ok(self.include_regex(include_regex))
162    }
163
164    /// The regexes to match group names against.
165    #[cfg(feature = "regex")]
166    #[must_use]
167    pub fn include_regex(mut self, include: Vec<regex::Regex>) -> Self {
168        if let Some(include_regex) = &mut self.include_regex {
169            include_regex.extend(include);
170        } else {
171            self.include_regex = Some(include);
172        }
173        self
174    }
175}
176
177impl Iterator for CgroupsV2Iterator {
178    type Item = Cgroup;
179
180    fn next(&mut self) -> Option<Self::Item> {
181        loop {
182            let entry = self.walker.next();
183            match entry {
184                Some(Ok(entry)) => {
185                    let path = entry.path();
186                    if !entry.file_type().is_dir() {
187                        continue;
188                    }
189                    let Ok(relative_path) = path.strip_prefix(&self.base_path) else {
190                        continue;
191                    };
192                    if relative_path.components().count() == 0 {
193                        continue;
194                    }
195                    if !path_matches_include(&self.include, relative_path) {
196                        continue;
197                    }
198                    #[cfg(feature = "regex")]
199                    if !path_matches_include_regex(&self.include_regex, relative_path) {
200                        continue;
201                    }
202                    return Some(Cgroup::load(Box::new(V2::new()), relative_path));
203                }
204                Some(Err(_e)) => return None,
205                None => return None,
206            }
207        }
208    }
209}
210
211impl Iterator for CgroupsV1Iterator {
212    type Item = Cgroup;
213
214    fn next(&mut self) -> Option<Self::Item> {
215        self.discovered
216            .next()
217            .map(|path| Cgroup::load(Box::new(V1::new()), path))
218    }
219}
220
221fn path_matches_include(include: &[glob::Pattern], path: &Path) -> bool {
222    if include.is_empty() {
223        return true;
224    }
225    let path_str = path.to_string_lossy();
226    include.iter().any(|pattern| pattern.matches(&path_str))
227}
228
229#[cfg(feature = "regex")]
230fn path_matches_include_regex(include: &[regex::Regex], path: &Path) -> bool {
231    if include.is_empty() {
232        return true;
233    }
234    let path_str = path.to_string_lossy();
235    include.iter().any(|pattern| pattern.is_match(&path_str))
236}
237
238fn parse_include(include: Vec<String>) -> Result<Vec<glob::Pattern>, ExplorerBuilderError> {
239    if include.is_empty() {
240        Ok(Vec::new())
241    } else {
242        include
243            .into_iter()
244            .map(|include| {
245                glob::Pattern::new(&include)
246                    .map_err(|e| ExplorerBuilderError::ValidationError(e.to_string()))
247            })
248            .collect()
249    }
250}
251
252#[cfg(feature = "regex")]
253fn parse_include_regex<S: AsRef<str>>(
254    include: &[S],
255) -> Result<Vec<regex::Regex>, ExplorerBuilderError> {
256    if include.is_empty() {
257        Ok(Vec::new())
258    } else {
259        include
260            .iter()
261            .map(|include| {
262                regex::Regex::new(include.as_ref())
263                    .map_err(|e| ExplorerBuilderError::ValidationError(e.to_string()))
264            })
265            .collect()
266    }
267}