use std::{
fs::{self, File},
path::{Path, PathBuf},
};
use crate::{
parse::parse_01_bool,
v1::{self, Resources, SubsystemKind},
Error, ErrorKind, Pid, Result,
};
const NOTIFY_ON_RELEASE: &str = "notify_on_release";
const RELEASE_AGENT: &str = "release_agent";
macro_rules! _gen_doc {
($op: literal, $file: expr) => { concat!(
"# Errors
This file is present only in the root cgroup. If you call this method on a non-root cgroup, an error
is returned with kind [`ErrorKind::InvalidOperation`]. On the root cgroup, returns an error if
failed to ", $op, " `", $file, "` file of this cgroup.
[`ErrorKind::InvalidOperation`]: ../enum.ErrorKind.html#variant.InvalidOperation\n\n"
) };
}
pub trait Cgroup {
fn new(path: CgroupPath) -> Self;
fn subsystem(&self) -> SubsystemKind;
fn path(&self) -> PathBuf;
fn is_root(&self) -> bool;
fn root_cgroup(&self) -> Box<Self>;
fn create(&mut self) -> Result<()> {
fs::create_dir(self.path()).map_err(Into::into)
}
fn apply(&mut self, resources: &Resources) -> Result<()>;
fn delete(&mut self) -> Result<()> {
fs::remove_dir(self.path()).map_err(Into::into)
}
gen_getter!(
cgroup;
"tasks",
"a list of tasks attached to this cgroup,"
: "The resulting tasks are represented by their thread IDs.",
tasks,
Vec<Pid>,
parse_tasks_procs
);
with_doc! { concat!(
"Attaches a task to this cgroup by writing a thread ID to `tasks` file.\n\n",
gen_doc!(see),
gen_doc!(err_write; "tasks"),
gen_doc!(eg_write; cpu, add_task, std::process::id())),
fn add_task(&mut self, pid: impl Into<Pid>) -> Result<()> {
fs::write(self.path().join("tasks"), format!("{}", pid.into())).map_err(Into::into)
}
}
fn remove_task(&mut self, pid: impl Into<Pid>) -> Result<()> {
self.root_cgroup().add_task(pid)
}
gen_getter!(
cgroup;
"cgroup.procs",
"a list of processes attached to this cgroup,"
: "The resulting tasks are represented by their PIDs.",
procs,
Vec<Pid>,
parse_tasks_procs
);
with_doc! { concat!(
"Attaches a process to this cgroup, with all threads in the same thread group at once,
by writing a PID to `cgroup.procs` file.\n\n",
gen_doc!(see),
gen_doc!(err_write; "cgroup.procs"),
gen_doc!(eg_write; cpu, add_proc, std::process::id())),
fn add_proc(&mut self, pid: impl Into<Pid>) -> Result<()> {
fs::write(self.path().join("cgroup.procs"), format!("{}", pid.into())).map_err(Into::into)
}
}
fn remove_proc(&mut self, pid: impl Into<Pid>) -> Result<()> {
self.root_cgroup().add_proc(pid)
}
gen_getter!(
cgroup;
"notify_on_release",
"whether the system executes the executable written in `release_agent` file
when this cgroup no longer has any task,",
notify_on_release,
bool,
parse_01_bool
);
with_doc! { concat!(
gen_doc!(
sets;
"notify_on_release",
"whether the system executes the executable written in `release_agent` file
when this cgroup no longer has any task,"
),
gen_doc!(see),
gen_doc!(err_write; "notify_on_release"),
gen_doc!(eg_write; cpu, set_notify_on_release, true)),
fn set_notify_on_release(&mut self, enable: bool) -> Result<()> {
fs::write(
self.path().join(NOTIFY_ON_RELEASE),
format!("{}", enable as i32),
)
.map_err(Into::into)
}
}
with_doc! { concat!(
gen_doc!(
reads;
"release_agent",
"the command to be executed when \"notify on release\" is triggered,
i.e. this cgroup is emptied of all tasks,"
),
gen_doc!(see),
_gen_doc!("read and parse", "release_agent"),
gen_doc!(eg_read; cpu, release_agent)),
fn release_agent(&self) -> Result<String> {
use std::io::Read;
if !self.is_root() {
return Err(Error::new(ErrorKind::InvalidOperation));
}
let mut buf = String::new();
self.open_file_read(RELEASE_AGENT)?
.read_to_string(&mut buf)?;
Ok(buf)
}
}
with_doc! { concat!(
gen_doc!(
sets;
"release_agent",
"a command to be executed when \"notify on release\" is triggered,
i.e. this cgroup is emptied of all tasks,"
),
gen_doc!(see),
_gen_doc!("write to", "release_agent"),
gen_doc!(eg_write; cpu, set_release_agent, b"/user/local/bin/foo.sh")),
fn set_release_agent(&mut self, agent_path: impl AsRef<[u8]>) -> Result<()> {
if !self.is_root() {
return Err(Error::new(ErrorKind::InvalidOperation));
}
fs::write(self.path().join(RELEASE_AGENT), agent_path.as_ref()).map_err(Into::into)
}
}
with_doc! { concat!(
gen_doc!(
reads;
"cgroup.sane_behavior",
"whether the subsystem of this cgroup is forced to follow \"sane behavior\","
),
gen_doc!(see),
_gen_doc!("read and parse", "cgroup.sane_behavior"),
gen_doc!(eg_read; cpu, sane_behavior)),
fn sane_behavior(&self) -> Result<bool> {
if !self.is_root() {
return Err(Error::new(ErrorKind::InvalidOperation));
}
self.open_file_read("cgroup.sane_behavior").and_then(parse_01_bool)
}
}
fn file_exists(&self, name: &str) -> bool {
self.path().join(name).exists()
}
fn open_file_read(&self, name: &str) -> Result<File> {
File::open(self.path().join(name)).map_err(Into::into)
}
fn open_file_write(&mut self, name: &str) -> Result<File> {
fs::OpenOptions::new()
.write(true)
.open(self.path().join(name))
.map_err(Into::into)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CgroupPath {
subsystem_root: PathBuf, name: Option<PathBuf>, }
impl CgroupPath {
pub fn new(kind: SubsystemKind, name: PathBuf) -> Self {
Self::with_subsystem_name(kind, name)
}
pub fn with_subsystem_name(subsystem_name: impl AsRef<Path>, name: PathBuf) -> Self {
assert!(
!subsystem_name.as_ref().as_os_str().is_empty(),
"Subsystem name must not be empty"
);
Self {
subsystem_root: Path::new(v1::CGROUPFS_MOUNT_POINT).join(subsystem_name),
name: if name.as_os_str().is_empty() {
None
} else {
Some(name)
},
}
}
pub(crate) fn to_path_buf(&self) -> PathBuf {
if let Some(ref name) = self.name {
self.subsystem_root.join(name)
} else {
self.subsystem_root.clone()
}
}
pub(crate) fn is_subsystem_root(&self) -> bool {
self.name.is_none()
}
pub(crate) fn subsystem_root(&self) -> Self {
Self {
subsystem_root: self.subsystem_root.clone(),
name: None,
}
}
}
macro_rules! impl_cgroup {
($subsystem: ident, $kind: ident, $( $tt: tt )*) => {
impl crate::v1::Cgroup for $subsystem {
fn new(path: crate::v1::CgroupPath) -> Self {
Self { path }
}
fn subsystem(&self) -> crate::v1::SubsystemKind {
crate::v1::SubsystemKind::$kind
}
fn path(&self) -> PathBuf {
self.path.to_path_buf()
}
fn is_root(&self) -> bool {
self.path.is_subsystem_root()
}
fn root_cgroup(&self) -> Box<Self> {
Box::new(Self::new(self.path.subsystem_root()))
}
$( $tt )*
}
};
}
pub(crate) trait CgroupHelper: Cgroup {
fn write_file(&mut self, name: &str, val: impl std::fmt::Display) -> Result<()> {
fs::write(self.path().join(name), format!("{}", val)).map_err(Into::into)
}
}
impl<T: Cgroup> CgroupHelper for T {}
fn parse_tasks_procs(reader: impl std::io::Read) -> Result<Vec<Pid>> {
use std::io::{BufRead, BufReader};
let mut ids = vec![];
for line in BufReader::new(reader).lines() {
let id = line?.trim().parse::<u32>()?;
ids.push(Pid::from(id))
}
Ok(ids)
}
#[cfg(test)]
mod tests {
use super::*;
use v1::cpu;
#[test]
fn test_cgroup_subsystem() {
macro_rules! t {
( $( ($subsystem: ident, $kind: ident) ),* $(, )? ) => {{ $(
let cgroup = v1::$subsystem::Subsystem::new(
CgroupPath::new(SubsystemKind::$kind, gen_cgroup_name!()));
assert_eq!(cgroup.subsystem(), SubsystemKind::$kind);
)* }};
}
t! {
(cpu, Cpu),
(cpuset, Cpuset),
(cpuacct, Cpuacct),
(memory, Memory),
(hugetlb, HugeTlb),
(devices, Devices),
(blkio, BlkIo),
(rdma, Rdma),
(net_prio, NetPrio),
(net_cls, NetCls),
(pids, Pids),
(freezer, Freezer),
(perf_event, PerfEvent),
}
}
#[test]
fn test_cgroup_create_delete() -> Result<()> {
let mut cgroup =
cpu::Subsystem::new(CgroupPath::new(SubsystemKind::Cpu, gen_cgroup_name!()));
assert!(!cgroup.path().exists());
cgroup.create()?;
assert!(cgroup.path().exists());
cgroup.delete()?;
assert!(!cgroup.path().exists());
Ok(())
}
#[test]
#[ignore] fn test_cgroup_add_get_remove_tasks() -> Result<()> {
use std::process::{self, Command};
let mut cgroup =
cpu::Subsystem::new(CgroupPath::new(SubsystemKind::Cpu, gen_cgroup_name!()));
cgroup.create()?;
let pid = Pid::from(process::id());
cgroup.add_task(pid)?;
assert_eq!(cgroup.tasks()?, vec![pid]);
let mut child = Command::new("sleep").arg("1").spawn().unwrap();
let child_pid = Pid::from(&child);
cgroup.add_task(child_pid)?; assert!(cgroup.tasks()? == vec![pid, child_pid] || cgroup.tasks()? == vec![child_pid, pid]);
child.wait()?;
assert!(cgroup.tasks()? == vec![pid]);
cgroup.remove_task(pid)?;
assert!(cgroup.tasks()?.is_empty());
cgroup.delete()
}
#[test]
#[ignore] fn test_cgroup_add_get_remove_procs() -> Result<()> {
use std::process::{self, Command};
let mut cgroup =
cpu::Subsystem::new(CgroupPath::new(SubsystemKind::Cpu, gen_cgroup_name!()));
cgroup.create()?;
let pid = Pid::from(process::id());
cgroup.add_proc(pid)?;
assert_eq!(cgroup.procs()?, vec![pid]);
let mut child = Command::new("sleep").arg("1").spawn().unwrap();
let child_pid = Pid::from(&child);
assert!(cgroup.procs()? == vec![pid, child_pid] || cgroup.procs()? == vec![child_pid, pid]);
child.wait()?;
assert!(cgroup.procs()? == vec![pid]);
cgroup.remove_proc(pid)?;
assert!(cgroup.procs()?.is_empty());
cgroup.delete()
}
#[test]
fn test_cgroup_notify_on_release() -> Result<()> {
let mut cgroup =
cpu::Subsystem::new(CgroupPath::new(SubsystemKind::Cpu, gen_cgroup_name!()));
cgroup.create()?;
assert_eq!(cgroup.notify_on_release()?, false);
cgroup.set_notify_on_release(true)?;
assert_eq!(cgroup.notify_on_release()?, true);
cgroup.delete()
}
#[test]
#[ignore] fn test_cgroup_release_agent() -> Result<()> {
let mut root = cpu::Subsystem::new(CgroupPath::new(SubsystemKind::Cpu, PathBuf::new()));
let agent = root.release_agent()?;
root.set_release_agent(b"foo")?;
assert_eq!(root.release_agent()?, "foo\n".to_string());
root.set_release_agent(&agent)?;
assert_eq!(root.release_agent()?, agent);
Ok(())
}
#[test]
fn err_cgroup_release_agent() -> Result<()> {
let mut cgroup =
cpu::Subsystem::new(CgroupPath::new(SubsystemKind::Cpu, gen_cgroup_name!()));
cgroup.create()?;
assert_eq!(
cgroup.release_agent().unwrap_err().kind(),
ErrorKind::InvalidOperation
);
assert_eq!(
cgroup.set_release_agent(b"foo").unwrap_err().kind(),
ErrorKind::InvalidOperation
);
cgroup.delete()
}
#[test]
fn test_cgroup_sane_behavior() -> Result<()> {
let root = cpu::Subsystem::new(CgroupPath::new(SubsystemKind::Cpu, PathBuf::new()));
assert_eq!(root.sane_behavior()?, false);
Ok(())
}
#[test]
fn err_cgroup_sane_behavior() -> Result<()> {
let mut cgroup =
cpu::Subsystem::new(CgroupPath::new(SubsystemKind::Cpu, gen_cgroup_name!()));
cgroup.create()?;
assert_eq!(
cgroup.sane_behavior().unwrap_err().kind(),
ErrorKind::InvalidOperation
);
cgroup.delete()
}
#[test]
fn test_cgroup_file_exists() -> Result<()> {
let root = cpu::Subsystem::new(CgroupPath::new(SubsystemKind::Cpu, PathBuf::new()));
assert!([
"tasks",
"cgroup.procs",
NOTIFY_ON_RELEASE,
RELEASE_AGENT,
"cgroup.sane_behavior",
]
.iter()
.all(|f| root.file_exists(f)));
assert!(!root.file_exists("does_not_exist"));
let files = ["tasks", "cgroup.procs", NOTIFY_ON_RELEASE];
let mut cgroup =
cpu::Subsystem::new(CgroupPath::new(SubsystemKind::Cpu, gen_cgroup_name!()));
cgroup.create()?;
assert!(files.iter().all(|f| cgroup.file_exists(f)));
assert!(!cgroup.file_exists("does_not_exist"));
cgroup.delete()?;
assert!(files.iter().all(|f| !cgroup.file_exists(f)));
Ok(())
}
#[test]
fn test_cgroup_open_file_read_write() -> Result<()> {
use std::io::{Read, Write};
let mut cgroup =
cpu::Subsystem::new(CgroupPath::new(SubsystemKind::Cpu, gen_cgroup_name!()));
cgroup.create()?;
let mut buf = String::new();
cgroup
.open_file_read(NOTIFY_ON_RELEASE)?
.read_to_string(&mut buf)
.unwrap();
assert_eq!(buf, "0\n");
let mut file = cgroup.open_file_write(NOTIFY_ON_RELEASE)?;
write!(file, "1").unwrap();
buf.clear();
cgroup
.open_file_read(NOTIFY_ON_RELEASE)?
.read_to_string(&mut buf)
.unwrap();
assert_eq!(buf, "1\n");
cgroup.delete()
}
#[test]
fn test_cgroup_path_new() {
let path = CgroupPath::new(SubsystemKind::Cpu, PathBuf::from("students/charlie"));
assert_eq!(
path.to_path_buf(),
PathBuf::from("/sys/fs/cgroup/cpu/students/charlie")
);
}
#[test]
fn test_cgroup_path_with_subsystem_name() {
let path = CgroupPath::with_subsystem_name("cpu_memory", PathBuf::from("students/charlie"));
assert_eq!(
path.to_path_buf(),
PathBuf::from("/sys/fs/cgroup/cpu_memory/students/charlie")
);
}
#[test]
#[should_panic]
fn panic_cgroup_path_with_subsystem_name() {
CgroupPath::with_subsystem_name("", PathBuf::from("students/charlie"));
}
#[test]
fn test_cgroup_path_subsystem_root() {
let path = CgroupPath::new(SubsystemKind::Cpu, PathBuf::from("students/charlie"));
assert!(!path.is_subsystem_root());
let root = path.subsystem_root();
assert!(root.is_subsystem_root());
assert_eq!(root.to_path_buf(), PathBuf::from("/sys/fs/cgroup/cpu"),);
}
#[test]
fn test_parse_tasks_procs() -> Result<()> {
const CONTENT_OK: &str = "\
1
2
3
";
assert_eq!(
parse_tasks_procs(CONTENT_OK.as_bytes())?,
vec![1.into(), 2.into(), 3.into()]
);
const CONTENT_NG: &str = "\
1
2
invalid
";
assert_eq!(
parse_tasks_procs(CONTENT_NG.as_bytes()).unwrap_err().kind(),
ErrorKind::Parse
);
Ok(())
}
}