use std::fs::{self};
use std::io::BufRead;
use std::path::{Path, PathBuf};
use std::time::{Duration, Instant};
use anyhow::{anyhow, bail, Ok};
use itertools::Itertools;
use nix::sys::statfs;
use nix::unistd::Pid;
use walkdir::WalkDir;
const KILLING_TIMEOUT: Duration = Duration::from_secs(1);
#[derive(Debug)]
pub struct CGroup {
path: PathBuf,
}
impl CGroup {
pub fn new_root<P: AsRef<Path>>(path: P, name: &str) -> anyhow::Result<Self> {
trace!("Create cgroup \"{name}\"");
if !is_cgroup(path.as_ref())? {
bail!("{} is not a valid cgroup", path.as_ref().display());
}
let path = PathBuf::from(path.as_ref()).join(name);
if path.exists() {
bail!("CGroup {path:?} already exists");
} else {
fs::create_dir(&path)?;
}
Self::import_root(&path)
}
pub fn import_root<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
trace!("Import cgroup {}", path.as_ref().display());
let path = PathBuf::from(path.as_ref());
if !is_cgroup(&path)? {
bail!("{} is not a valid cgroup", path.display());
}
Ok(CGroup { path })
}
pub fn new(&self, name: &str) -> anyhow::Result<Self> {
Self::new_root(&self.path, name)
}
pub fn new_threaded(&self, name: &str) -> anyhow::Result<Self> {
let cgroup = Self::new_root(&self.path, name)?;
cgroup.set_threaded()?;
Ok(cgroup)
}
pub fn mv_proc(&self, pid: Pid) -> anyhow::Result<()> {
trace!("Move {pid:?} to {}", self.get_path().display());
if !is_cgroup(&self.path)? {
bail!("{} is not a valid cgroup", self.path.display());
}
fs::write(self.path.join("cgroup.procs"), pid.to_string())?;
Ok(())
}
pub fn mv_thread(&self, pid: Pid) -> anyhow::Result<()> {
trace!("Move {pid:?} to {}", self.get_path().display());
if !is_cgroup(&self.path)? {
bail!("{} is not a valid cgroup", self.path.display());
}
fs::write(self.path.join("cgroup.threads"), pid.to_string())?;
Ok(())
}
fn set_threaded(&self) -> anyhow::Result<()> {
trace!("Change type of {} to threaded", self.get_path().display());
if !is_cgroup(&self.path)? {
bail!("{} is not a valid cgroup", self.path.display());
}
fs::write(self.path.join("cgroup.type"), "threaded")?;
Ok(())
}
pub fn get_pids(&self) -> anyhow::Result<Vec<Pid>> {
if !is_cgroup(&self.path)? {
bail!("{} is not a valid cgroup", self.path.display());
}
let pids: Vec<Pid> = fs::read(self.path.join("cgroup.procs"))?
.lines()
.map(|line| Pid::from_raw(line.unwrap().parse().unwrap()))
.collect();
Ok(pids)
}
pub fn get_tids(&self) -> anyhow::Result<Vec<Pid>> {
if !is_cgroup(&self.path)? {
bail!("{} is not a valid cgroup", self.path.display());
}
let pids: Vec<Pid> = fs::read(self.path.join("cgroup.threads"))?
.lines()
.map(|line| Pid::from_raw(line.unwrap().parse().unwrap()))
.collect();
Ok(pids)
}
pub fn populated(&self) -> anyhow::Result<bool> {
if !is_cgroup(&self.path)? {
bail!("{} is not a valid cgroup", self.path.display());
}
Ok(fs::read_to_string(self.get_events_path())?.contains("populated 1\n"))
}
pub fn frozen(&self) -> anyhow::Result<bool> {
if !is_cgroup(&self.path)? {
bail!("{} is not a valid cgroup", self.path.display());
}
let path = self.path.join("cgroup.freeze");
if !path.exists() {
return Ok(false);
}
Ok(fs::read(&path)? == b"1\n")
}
pub fn freeze(&self) -> anyhow::Result<()> {
trace!("Freeze {}", self.get_path().display());
if !is_cgroup(&self.path)? {
bail!("{} is not a valid cgroup", self.path.display());
}
let path = self.path.join("cgroup.freeze");
if !path.exists() {
bail!("cannot freeze the root cgroup");
}
Ok(fs::write(path, "1")?)
}
pub fn unfreeze(&self) -> anyhow::Result<()> {
trace!("Unfreeze {}", self.get_path().display());
if !is_cgroup(&self.path)? {
bail!("{} is not a valid cgroup", self.path.display());
}
let path = self.path.join("cgroup.freeze");
if !path.exists() {
bail!("cannot unfreeze the root cgroup");
}
Ok(fs::write(path, "0")?)
}
pub fn kill(&self) -> anyhow::Result<()> {
trace!("Kill {}", self.get_path().display());
if !is_cgroup(&self.path)? {
bail!("{} is not a valid cgroup", self.path.display());
}
let killfile = self.path.join("cgroup.kill");
if !killfile.exists() {
bail!("cannot kill the root cgroup");
}
trace!("writing '1' to {}", killfile.display());
fs::write(killfile, "1")?;
let start = Instant::now();
trace!("Killing with a {KILLING_TIMEOUT:?} timeout");
while start.elapsed() < KILLING_TIMEOUT {
if !self.populated()? {
trace!("Killed with a {KILLING_TIMEOUT:?} timeout");
return Ok(());
}
}
bail!("failed to kill the cgroup")
}
pub fn get_path(&self) -> PathBuf {
self.path.clone()
}
pub fn get_events_path(&self) -> PathBuf {
self.path.join("cgroup.events")
}
pub fn rm(&self) -> anyhow::Result<()> {
trace!("Remove {}", self.get_path().display());
if !is_cgroup(&self.path)? {
bail!("{} is not a valid cgroup", self.path.display());
}
self.kill()?;
trace!("Calling remove on {}", &self.path.display());
for d in WalkDir::new(&self.path)
.into_iter()
.flatten()
.filter(|e| e.file_type().is_dir())
.sorted_by(|a, b| a.depth().cmp(&b.depth()).reverse())
{
trace!("Removing cgroup {}", &d.path().display());
fs::remove_dir(d.path())?;
}
Ok(())
}
}
pub fn mount_point() -> anyhow::Result<PathBuf> {
procfs::process::Process::myself()?
.mountinfo()?
.into_iter()
.find(|m| m.fs_type.eq("cgroup2")) .ok_or_else(|| anyhow!("no cgroup2 mount found"))
.map(|m| m.mount_point.clone())
}
pub fn current_cgroup() -> anyhow::Result<PathBuf> {
let path = procfs::process::Process::myself()?
.cgroups()?
.into_iter()
.next()
.ok_or(anyhow!("cannot obtain cgroup"))?
.pathname
.clone();
let path = &path[1..path.len()]; Ok(PathBuf::from(path))
}
fn is_cgroup(path: &Path) -> anyhow::Result<bool> {
let st = statfs::statfs(path)?;
Ok(st.filesystem_type() == statfs::CGROUP2_SUPER_MAGIC)
}
#[cfg(test)]
mod tests {
use std::{io, process};
use super::*;
#[test]
fn new_root() {
let name = gen_name();
let path = get_path().join(&name);
assert!(!path.exists()); let cg = CGroup::new_root(get_path(), &name).unwrap();
assert!(path.exists() && path.is_dir());
cg.rm().unwrap();
assert!(!path.exists());
}
#[test]
fn import_root() {
let path = get_path().join(gen_name());
assert!(!path.exists()); fs::create_dir(&path).unwrap();
let cg = CGroup::import_root(&path).unwrap();
cg.rm().unwrap();
assert!(!path.exists());
}
#[test]
fn new() {
let name1 = gen_name();
let name2 = gen_name();
let path_cg1 = get_path().join(&name1);
let path_cg2 = path_cg1.join(&name2);
assert!(!path_cg1.exists()); let cg1 = CGroup::new_root(get_path(), &name1).unwrap();
assert!(path_cg1.exists() && path_cg1.is_dir());
assert!(!path_cg2.exists());
let _cg2 = cg1.new(&name2).unwrap();
assert!(path_cg2.exists() && path_cg2.is_dir());
cg1.rm().unwrap();
assert!(!path_cg2.exists());
assert!(!path_cg1.exists());
}
#[test]
fn mv() {
let mut proc = spawn_proc().unwrap();
let pid = Pid::from_raw(proc.id() as i32);
let cg1 = CGroup::new_root(get_path(), &gen_name()).unwrap();
let cg2 = cg1.new(&gen_name()).unwrap();
cg1.mv_proc(pid).unwrap();
cg2.mv_proc(pid).unwrap();
proc.kill().unwrap();
cg1.rm().unwrap();
}
#[test]
fn get_pids() {
let mut proc = spawn_proc().unwrap();
let pid = Pid::from_raw(proc.id() as i32);
let cg1 = CGroup::new_root(get_path(), &gen_name()).unwrap();
let cg2 = cg1.new(&gen_name()).unwrap();
assert!(cg1.get_pids().unwrap().is_empty());
assert!(cg2.get_pids().unwrap().is_empty());
cg1.mv_proc(pid).unwrap();
let pids = cg1.get_pids().unwrap();
assert!(!pids.is_empty());
assert!(cg2.get_pids().unwrap().is_empty());
assert_eq!(pids.len(), 1);
assert_eq!(pids[0], pid);
cg2.mv_proc(pid).unwrap();
let pids = cg2.get_pids().unwrap();
assert!(!pids.is_empty());
assert!(cg1.get_pids().unwrap().is_empty());
assert_eq!(pids.len(), 1);
assert_eq!(pids[0], pid);
proc.kill().unwrap();
cg1.rm().unwrap();
}
#[test]
fn populated() {
let mut proc = spawn_proc().unwrap();
let pid = Pid::from_raw(proc.id() as i32);
let cg = CGroup::new_root(get_path(), &gen_name()).unwrap();
assert!(!cg.populated().unwrap());
assert_eq!(cg.populated().unwrap(), !cg.get_pids().unwrap().is_empty());
cg.mv_proc(pid).unwrap();
assert!(cg.populated().unwrap());
assert_eq!(cg.populated().unwrap(), !cg.get_pids().unwrap().is_empty());
proc.kill().unwrap();
cg.rm().unwrap();
}
#[test]
fn frozen() {
let mut proc = spawn_proc().unwrap();
let pid = Pid::from_raw(proc.id() as i32);
let cg = CGroup::new_root(get_path(), &gen_name()).unwrap();
assert!(!cg.frozen().unwrap());
cg.freeze().unwrap();
assert!(cg.frozen().unwrap());
cg.unfreeze().unwrap();
assert!(!cg.frozen().unwrap());
cg.mv_proc(pid).unwrap();
cg.freeze().unwrap();
assert!(cg.frozen().unwrap());
cg.unfreeze().unwrap();
assert!(!cg.frozen().unwrap());
proc.kill().unwrap();
cg.rm().unwrap();
}
#[test]
fn kill() {
let proc = spawn_proc().unwrap();
let pid = Pid::from_raw(proc.id() as i32);
let cg = CGroup::new_root(get_path(), &gen_name()).unwrap();
cg.kill().unwrap();
cg.mv_proc(pid).unwrap();
assert!(cg.populated().unwrap());
cg.kill().unwrap();
cg.rm().unwrap();
}
#[test]
fn is_cgroup() {
assert!(super::is_cgroup(&get_path()).unwrap());
assert!(!super::is_cgroup(Path::new("/tmp")).unwrap());
}
fn spawn_proc() -> io::Result<process::Child> {
process::Command::new("sleep")
.arg("120")
.stdout(process::Stdio::null())
.spawn()
}
fn get_path() -> PathBuf {
super::mount_point()
.unwrap()
.join(super::current_cgroup().unwrap())
}
fn gen_name() -> String {
loop {
let val: u64 = rand::random();
let str = format!("apex-test-{val}");
if !Path::new(&str).exists() {
return str;
}
}
}
}