1use crate::cgroup::UNLIMIT_CGROUP_NAME;
2use crate::CgroupManager;
3use common::Result;
4use std::fs;
5use std::path::Path;
6
7#[derive(Debug)]
8pub struct ProcessStatus {
9 pub pid: u32,
10 pub name: String,
11 pub cgroup_name: String,
12 pub memory_max: Option<u64>,
13 pub cpu_quota: Option<u32>,
14 pub io_read_bps: Option<u64>,
15 pub io_write_bps: Option<u64>,
16 pub is_shared: bool,
17 pub process_count: Option<usize>,
18 pub populated: bool,
19}
20
21fn managed_cgroup_names(manager: &CgroupManager) -> Result<Vec<String>> {
25 let base = manager.base_path();
26 if !base.exists() {
27 return Ok(Vec::new());
28 }
29 let mut names = Vec::new();
30 for entry in fs::read_dir(base)? {
31 let entry = entry?;
32 if !entry.path().is_dir() {
33 continue;
34 }
35 let Some(name) = entry.file_name().to_str().map(str::to_string) else {
36 continue;
37 };
38 if name == UNLIMIT_CGROUP_NAME {
39 continue;
40 }
41 if ["pid-", "app-", "multi-", "run-", "gtk-"]
42 .iter()
43 .any(|p| name.starts_with(p))
44 {
45 names.push(name);
46 }
47 }
48 names.sort();
49 Ok(names)
50}
51
52pub fn get_managed_processes(manager: &CgroupManager) -> Result<Vec<ProcessStatus>> {
57 let base = manager.base_path();
58 let mut results = Vec::new();
59
60 for cgroup_name in managed_cgroup_names(manager)? {
61 if manager.is_populated(&cgroup_name) != Some(true) {
62 continue;
63 }
64 let path = base.join(&cgroup_name);
65
66 let memory_max = parse_memory_max(&path);
67 let cpu_quota = parse_cpu_quota(&path);
68 let (io_read_bps, io_write_bps) = parse_io_limits(&path);
69
70 if memory_max.is_none()
72 && cpu_quota.is_none()
73 && io_read_bps.is_none()
74 && io_write_bps.is_none()
75 {
76 continue;
77 }
78
79 let pid = read_first_pid(&path).unwrap_or(0);
80 let name = fs::read_to_string(format!("/proc/{pid}/comm"))
81 .ok()
82 .filter(|_| pid != 0)
83 .map(|s| s.trim().to_string())
84 .unwrap_or_else(|| "?".to_string());
85
86 let is_shared = !cgroup_name.starts_with("pid-");
87 let process_count = if is_shared {
88 fs::read_to_string(path.join("cgroup.procs"))
89 .ok()
90 .map(|c| c.lines().filter(|l| !l.trim().is_empty()).count())
91 } else {
92 None
93 };
94
95 results.push(ProcessStatus {
96 pid,
97 name,
98 cgroup_name,
99 memory_max,
100 cpu_quota,
101 io_read_bps,
102 io_write_bps,
103 is_shared,
104 process_count,
105 populated: true,
106 });
107 }
108
109 Ok(results)
110}
111
112pub fn empty_cgroups(manager: &CgroupManager) -> Vec<String> {
115 managed_cgroup_names(manager)
116 .unwrap_or_default()
117 .into_iter()
118 .filter(|n| manager.is_populated(n) == Some(false))
119 .collect()
120}
121
122fn read_first_pid(cgroup_path: &Path) -> Option<u32> {
123 let content = fs::read_to_string(cgroup_path.join("cgroup.procs")).ok()?;
124 content.lines().next()?.trim().parse().ok()
125}
126
127fn parse_memory_max(cgroup_path: &Path) -> Option<u64> {
128 let content = fs::read_to_string(cgroup_path.join("memory.max")).ok()?;
129 let content = content.trim();
130 if content == "max" {
131 return None;
132 }
133 content.parse().ok()
134}
135
136fn parse_cpu_quota(cgroup_path: &Path) -> Option<u32> {
137 let content = fs::read_to_string(cgroup_path.join("cpu.max")).ok()?;
138 let content = content.trim();
139 if content == "max" || content.starts_with("max ") {
140 return None;
141 }
142
143 let mut parts = content.split_whitespace();
145 let quota: u64 = parts.next()?.parse().ok()?;
146 let period: u64 = parts.next()?.parse().ok()?;
147
148 if period == 0 {
149 return None;
150 }
151
152 Some(quota.saturating_mul(100).saturating_div(period) as u32)
154}
155
156fn parse_io_limits(cgroup_path: &Path) -> (Option<u64>, Option<u64>) {
157 let content = match fs::read_to_string(cgroup_path.join("io.max")) {
158 Ok(c) => c,
159 Err(_) => return (None, None),
160 };
161
162 let mut read_bps = None;
163 let mut write_bps = None;
164
165 for line in content.lines() {
167 for part in line.split_whitespace().skip(1) {
168 if let Some(val) = part.strip_prefix("rbps=") {
169 if val != "max" {
170 read_bps = read_bps.or_else(|| val.parse().ok());
171 }
172 } else if let Some(val) = part.strip_prefix("wbps=") {
173 if val != "max" {
174 write_bps = write_bps.or_else(|| val.parse().ok());
175 }
176 }
177 }
178 }
179
180 (read_bps, write_bps)
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use std::fs;
187
188 fn cg(dir: &Path, name: &str, procs: &str, populated: bool) {
189 let p = dir.join(name);
190 fs::create_dir(&p).unwrap();
191 fs::write(p.join("cgroup.procs"), procs).unwrap();
192 fs::write(
193 p.join("cgroup.events"),
194 format!("populated {}\nfrozen 0\n", u8::from(populated)),
195 )
196 .unwrap();
197 fs::write(p.join("memory.max"), "104857600\n").unwrap();
198 }
199
200 #[test]
201 fn status_is_read_only() {
202 let dir = tempfile::tempdir().unwrap();
203 cg(dir.path(), "run-1-2", "", false);
204 let m = CgroupManager::at(dir.path().to_path_buf());
205 assert!(get_managed_processes(&m).unwrap().is_empty());
206 assert!(
207 dir.path().join("run-1-2").exists(),
208 "status must never remove cgroups"
209 );
210 assert_eq!(empty_cgroups(&m), vec!["run-1-2".to_string()]);
211 }
212
213 #[test]
214 fn populated_cgroup_is_listed_even_if_its_first_pid_is_gone() {
215 let dir = tempfile::tempdir().unwrap();
216 cg(dir.path(), "app-firefox", "999999999\n", true);
217 let m = CgroupManager::at(dir.path().to_path_buf());
218 let s = get_managed_processes(&m).unwrap();
219 assert_eq!(s.len(), 1);
220 assert_eq!(s[0].name, "?");
221 assert!(s[0].populated);
222 }
223}