use std::fs;
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use std::thread;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
pub struct CGroup {
dir: PathBuf,
}
#[derive(Debug, Default)]
pub struct CGroupStats {
pub peak_memory_bytes: Option<u64>,
pub disk_read_bytes: Option<u64>,
pub disk_write_bytes: Option<u64>,
}
static RUN_PARENT: OnceLock<Option<PathBuf>> = OnceLock::new();
impl CGroup {
pub fn try_create() -> Option<CGroup> {
let parent = RUN_PARENT.get_or_init(setup_run_parent).clone()?;
let dir = parent.join(format!(
"hyperfoot-run-{}-{}",
std::process::id(),
unique_suffix()
));
fs::create_dir(&dir).ok()?;
Some(CGroup { dir })
}
pub fn procs_path(&self) -> PathBuf {
self.dir.join("cgroup.procs")
}
pub fn wait_drain(&self) {
let procs_path = self.dir.join("cgroup.procs");
for _ in 0..50 {
match fs::read_to_string(&procs_path) {
Ok(contents) if contents.trim().is_empty() => return,
Err(_) => return,
_ => thread::sleep(Duration::from_millis(10)),
}
}
}
pub fn read_stats(&self) -> CGroupStats {
let peak_memory_bytes = fs::read_to_string(self.dir.join("memory.peak"))
.ok()
.and_then(|s| s.trim().parse::<u64>().ok());
let disk = fs::read_to_string(self.dir.join("io.stat"))
.ok()
.map(|contents| parse_io_stat(&contents));
CGroupStats {
peak_memory_bytes,
disk_read_bytes: disk.map(|(r, _)| r),
disk_write_bytes: disk.map(|(_, w)| w),
}
}
pub fn cleanup(self) {
let _ = fs::remove_dir(&self.dir);
}
}
fn parse_io_stat(contents: &str) -> (u64, u64) {
let mut read_bytes = 0u64;
let mut write_bytes = 0u64;
for line in contents.lines() {
for field in line.split_whitespace().skip(1) {
if let Some(v) = field.strip_prefix("rbytes=") {
read_bytes += v.parse::<u64>().unwrap_or(0);
} else if let Some(v) = field.strip_prefix("wbytes=") {
write_bytes += v.parse::<u64>().unwrap_or(0);
}
}
}
(read_bytes, write_bytes)
}
fn setup_run_parent() -> Option<PathBuf> {
let base = own_cgroup_dir()?;
cleanup_stale_leaves(&base);
let self_leaf = base.join(format!("hyperfoot-self-{}", std::process::id()));
fs::create_dir(&self_leaf).ok()?;
fs::write(
self_leaf.join("cgroup.procs"),
std::process::id().to_string(),
)
.ok()?;
enable_controllers(&base);
Some(base)
}
fn cleanup_stale_leaves(base: &Path) {
let Ok(entries) = fs::read_dir(base) else {
return;
};
for entry in entries.flatten() {
let name = entry.file_name();
if !name.to_string_lossy().starts_with("hyperfoot-") {
continue;
}
let path = entry.path();
let is_empty = fs::read_to_string(path.join("cgroup.procs"))
.map(|s| s.trim().is_empty())
.unwrap_or(false);
if is_empty {
let _ = fs::remove_dir(&path);
}
}
}
fn own_cgroup_dir() -> Option<PathBuf> {
let contents = fs::read_to_string("/proc/self/cgroup").ok()?;
let rel = contents.lines().find_map(|l| l.strip_prefix("0::"))?;
let path = Path::new("/sys/fs/cgroup").join(rel.trim_start_matches('/'));
path.is_dir().then_some(path)
}
fn enable_controllers(base: &Path) {
let Ok(controllers) = fs::read_to_string(base.join("cgroup.controllers")) else {
return;
};
let wanted = ["cpu", "memory", "io"];
let enable: Vec<String> = wanted
.into_iter()
.filter(|c| {
controllers
.split_whitespace()
.any(|available| available == *c)
})
.map(|c| format!("+{c}"))
.collect();
if !enable.is_empty() {
let _ = fs::write(base.join("cgroup.subtree_control"), enable.join(" "));
}
}
fn unique_suffix() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
}