use crate::cgroup::UNLIMIT_CGROUP_NAME;
use crate::CgroupManager;
use common::Result;
use std::fs;
use std::path::Path;
#[derive(Debug)]
pub struct ProcessStatus {
pub pid: u32,
pub name: String,
pub cgroup_name: String,
pub memory_max: Option<u64>,
pub cpu_quota: Option<u32>,
pub io_read_bps: Option<u64>,
pub io_write_bps: Option<u64>,
pub is_shared: bool,
pub process_count: Option<usize>,
pub populated: bool,
}
fn managed_cgroup_names(manager: &CgroupManager) -> Result<Vec<String>> {
let base = manager.base_path();
if !base.exists() {
return Ok(Vec::new());
}
let mut names = Vec::new();
for entry in fs::read_dir(base)? {
let entry = entry?;
if !entry.path().is_dir() {
continue;
}
let Some(name) = entry.file_name().to_str().map(str::to_string) else {
continue;
};
if name == UNLIMIT_CGROUP_NAME {
continue;
}
if ["pid-", "app-", "multi-", "run-", "gtk-"]
.iter()
.any(|p| name.starts_with(p))
{
names.push(name);
}
}
names.sort();
Ok(names)
}
pub fn get_managed_processes(manager: &CgroupManager) -> Result<Vec<ProcessStatus>> {
let base = manager.base_path();
let mut results = Vec::new();
for cgroup_name in managed_cgroup_names(manager)? {
if manager.is_populated(&cgroup_name) != Some(true) {
continue;
}
let path = base.join(&cgroup_name);
let memory_max = parse_memory_max(&path);
let cpu_quota = parse_cpu_quota(&path);
let (io_read_bps, io_write_bps) = parse_io_limits(&path);
if memory_max.is_none()
&& cpu_quota.is_none()
&& io_read_bps.is_none()
&& io_write_bps.is_none()
{
continue;
}
let pid = read_first_pid(&path).unwrap_or(0);
let name = fs::read_to_string(format!("/proc/{pid}/comm"))
.ok()
.filter(|_| pid != 0)
.map(|s| s.trim().to_string())
.unwrap_or_else(|| "?".to_string());
let is_shared = !cgroup_name.starts_with("pid-");
let process_count = if is_shared {
fs::read_to_string(path.join("cgroup.procs"))
.ok()
.map(|c| c.lines().filter(|l| !l.trim().is_empty()).count())
} else {
None
};
results.push(ProcessStatus {
pid,
name,
cgroup_name,
memory_max,
cpu_quota,
io_read_bps,
io_write_bps,
is_shared,
process_count,
populated: true,
});
}
Ok(results)
}
pub fn empty_cgroups(manager: &CgroupManager) -> Vec<String> {
managed_cgroup_names(manager)
.unwrap_or_default()
.into_iter()
.filter(|n| manager.is_populated(n) == Some(false))
.collect()
}
fn read_first_pid(cgroup_path: &Path) -> Option<u32> {
let content = fs::read_to_string(cgroup_path.join("cgroup.procs")).ok()?;
content.lines().next()?.trim().parse().ok()
}
fn parse_memory_max(cgroup_path: &Path) -> Option<u64> {
let content = fs::read_to_string(cgroup_path.join("memory.max")).ok()?;
let content = content.trim();
if content == "max" {
return None;
}
content.parse().ok()
}
fn parse_cpu_quota(cgroup_path: &Path) -> Option<u32> {
let content = fs::read_to_string(cgroup_path.join("cpu.max")).ok()?;
let content = content.trim();
if content == "max" || content.starts_with("max ") {
return None;
}
let mut parts = content.split_whitespace();
let quota: u64 = parts.next()?.parse().ok()?;
let period: u64 = parts.next()?.parse().ok()?;
if period == 0 {
return None;
}
Some(quota.saturating_mul(100).saturating_div(period) as u32)
}
fn parse_io_limits(cgroup_path: &Path) -> (Option<u64>, Option<u64>) {
let content = match fs::read_to_string(cgroup_path.join("io.max")) {
Ok(c) => c,
Err(_) => return (None, None),
};
let mut read_bps = None;
let mut write_bps = None;
for line in content.lines() {
for part in line.split_whitespace().skip(1) {
if let Some(val) = part.strip_prefix("rbps=") {
if val != "max" {
read_bps = read_bps.or_else(|| val.parse().ok());
}
} else if let Some(val) = part.strip_prefix("wbps=") {
if val != "max" {
write_bps = write_bps.or_else(|| val.parse().ok());
}
}
}
}
(read_bps, write_bps)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn cg(dir: &Path, name: &str, procs: &str, populated: bool) {
let p = dir.join(name);
fs::create_dir(&p).unwrap();
fs::write(p.join("cgroup.procs"), procs).unwrap();
fs::write(
p.join("cgroup.events"),
format!("populated {}\nfrozen 0\n", u8::from(populated)),
)
.unwrap();
fs::write(p.join("memory.max"), "104857600\n").unwrap();
}
#[test]
fn status_is_read_only() {
let dir = tempfile::tempdir().unwrap();
cg(dir.path(), "run-1-2", "", false);
let m = CgroupManager::at(dir.path().to_path_buf());
assert!(get_managed_processes(&m).unwrap().is_empty());
assert!(
dir.path().join("run-1-2").exists(),
"status must never remove cgroups"
);
assert_eq!(empty_cgroups(&m), vec!["run-1-2".to_string()]);
}
#[test]
fn populated_cgroup_is_listed_even_if_its_first_pid_is_gone() {
let dir = tempfile::tempdir().unwrap();
cg(dir.path(), "app-firefox", "999999999\n", true);
let m = CgroupManager::at(dir.path().to_path_buf());
let s = get_managed_processes(&m).unwrap();
assert_eq!(s.len(), 1);
assert_eq!(s[0].name, "?");
assert!(s[0].populated);
}
}