use anyhow::Context;
use log::{debug, warn};
use nix::unistd;
use std::collections::HashMap;
use std::fs::File;
use std::io::Write;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use crate::procfs;
use crate::result::Result;
trait CgroupManager {
fn move_to(&self, pid: unistd::Pid, target_pid: unistd::Pid) -> Result<()>;
}
struct CgroupV1Manager {
procfs_path: PathBuf,
}
struct CgroupV2Manager {
mount_path: PathBuf,
procfs_path: PathBuf,
}
struct HybridCgroupManager {
v1: CgroupV1Manager,
v2: CgroupV2Manager,
}
struct NullCgroupManager;
fn get_subsystems() -> Result<Vec<String>> {
let path = "/proc/cgroups";
let f = File::open(path).context("failed to open /proc/cgroups")?;
let reader = BufReader::new(f);
let mut subsystems: Vec<String> = Vec::new();
for l in reader.lines() {
let line = l.context("failed to read line from /proc/cgroups")?;
if line.starts_with('#') {
continue;
}
let fields: Vec<&str> = line.split('\t').collect();
if fields.len() >= 4 && fields[3] != "0" {
subsystems.push(fields[0].to_string());
}
}
Ok(subsystems)
}
fn get_mounts() -> Result<HashMap<String, String>> {
let subsystems =
get_subsystems().context("failed to obtain cgroup subsystems from /proc/cgroups")?;
let path = "/proc/self/mountinfo";
let f = File::open(path).context("failed to open /proc/self/mountinfo")?;
let reader = BufReader::new(f);
let mut mountpoints: HashMap<String, String> = HashMap::new();
for l in reader.lines() {
let line = l.with_context(|| format!("failed to read line from {}", path))?;
let fields: Vec<&str> = line.split(' ').collect();
if fields.len() < 11 || fields[9] != "cgroup" {
continue;
}
for option in fields[10].split(',') {
let name = option.strip_prefix("name=").unwrap_or(option).to_string();
if subsystems.contains(&name) {
mountpoints.insert(name, fields[4].to_string());
}
}
}
Ok(mountpoints)
}
fn cgroup_v1_path(cgroup: &str, mountpoints: &HashMap<String, String>) -> Option<PathBuf> {
for c in cgroup.split(',') {
let m = mountpoints.get(c);
if let Some(path) = m {
let mut tasks_path = PathBuf::from(path);
tasks_path.push(cgroup);
tasks_path.push("tasks");
return Some(tasks_path);
}
}
None
}
impl CgroupV1Manager {
fn get_cgroups(&self, pid: unistd::Pid) -> Result<Vec<String>> {
let path = self.procfs_path.join(format!("{}/cgroup", pid));
let f = File::open(&path)
.with_context(|| format!("failed to open cgroup file {}", path.display()))?;
let reader = BufReader::new(f);
let mut cgroups: Vec<String> = Vec::new();
for l in reader.lines() {
let line = l.with_context(|| format!("failed to read line from {}", path.display()))?;
let fields: Vec<&str> = line.split(":/").collect();
if fields.len() >= 2 {
cgroups.push(fields[1].to_string());
}
}
Ok(cgroups)
}
}
impl CgroupManager for CgroupV1Manager {
fn move_to(&self, pid: unistd::Pid, target_pid: unistd::Pid) -> Result<()> {
let cgroups = self
.get_cgroups(target_pid)
.with_context(|| format!("failed to get cgroups for PID {}", target_pid))?;
let mountpoints = get_mounts().context("failed to get cgroup mountpoints")?;
for cgroup in cgroups {
let p = cgroup_v1_path(&cgroup, &mountpoints);
if let Some(path) = p {
match File::create(&path) {
Ok(mut buffer) => {
write!(buffer, "{}", pid)
.with_context(|| format!("failed to write PID to cgroup {}", cgroup))?;
}
Err(err) => {
warn!("failed to enter {} cgroup: {}", cgroup, err);
}
}
}
}
Ok(())
}
}
impl CgroupV2Manager {
fn get_cgroup_path(&self, pid: unistd::Pid) -> Result<Option<String>> {
let path = self.procfs_path.join(format!("{}/cgroup", pid));
let f = File::open(&path)
.with_context(|| format!("failed to open cgroup file {}", path.display()))?;
let reader = BufReader::new(f);
for l in reader.lines() {
let line = l.with_context(|| format!("failed to read line from {}", path.display()))?;
if let Some(stripped) = line.strip_prefix("0::") {
return Ok(Some(stripped.to_string()));
}
}
Ok(None)
}
}
impl CgroupManager for CgroupV2Manager {
fn move_to(&self, pid: unistd::Pid, target_pid: unistd::Pid) -> Result<()> {
let target_cgroup = self
.get_cgroup_path(target_pid)
.with_context(|| format!("failed to get cgroup v2 path for PID {}", target_pid))?;
let Some(cgroup_path) = target_cgroup else {
warn!(
"PID {} not in a cgroup v2, skipping cgroup migration",
target_pid
);
return Ok(());
};
let mut procs_path = self.mount_path.clone();
procs_path.push(cgroup_path.trim_start_matches('/'));
procs_path.push("cgroup.procs");
match File::options().append(true).open(&procs_path) {
Ok(mut file) => {
if let Err(err) = write!(file, "{}", pid) {
warn!(
"failed to write PID to cgroup.procs at {}: {} (try running as root or with CAP_SYS_ADMIN)",
procs_path.display(),
err
);
}
}
Err(err) => {
warn!(
"failed to open cgroup.procs at {}: {}",
procs_path.display(),
err
);
}
}
Ok(())
}
}
impl CgroupManager for HybridCgroupManager {
fn move_to(&self, pid: unistd::Pid, target_pid: unistd::Pid) -> Result<()> {
if let Err(e) = self.v2.move_to(pid, target_pid) {
warn!("cgroup v2 migration failed: {}, trying v1", e);
self.v1.move_to(pid, target_pid)?;
}
Ok(())
}
}
impl CgroupManager for NullCgroupManager {
fn move_to(&self, _pid: unistd::Pid, _target_pid: unistd::Pid) -> Result<()> {
debug!("cgroup support not detected, skipping cgroup migration");
Ok(())
}
}
fn create_manager() -> Result<Box<dyn CgroupManager>> {
let path = "/proc/self/mountinfo";
let f = File::open(path).context("failed to open /proc/self/mountinfo")?;
let reader = BufReader::new(f);
let mut has_v1 = false;
let mut v2_mount: Option<PathBuf> = None;
for l in reader.lines() {
let line = l.with_context(|| format!("failed to read line from {}", path))?;
let fields: Vec<&str> = line.split(' ').collect();
if fields.len() < 10 {
continue;
}
if fields[9] == "cgroup" {
has_v1 = true;
} else if fields[9] == "cgroup2" {
v2_mount = Some(PathBuf::from(fields[4]));
}
}
let procfs_path = procfs::get_path();
match (has_v1, v2_mount) {
(true, Some(mount_path)) => {
Ok(Box::new(HybridCgroupManager {
v1: CgroupV1Manager {
procfs_path: procfs_path.clone(),
},
v2: CgroupV2Manager {
mount_path,
procfs_path,
},
}))
}
(true, None) => {
Ok(Box::new(CgroupV1Manager { procfs_path }))
}
(false, Some(mount_path)) => {
Ok(Box::new(CgroupV2Manager {
mount_path,
procfs_path,
}))
}
(false, None) => {
Ok(Box::new(NullCgroupManager))
}
}
}
pub(crate) fn move_to(pid: unistd::Pid, target_pid: unistd::Pid) -> Result<()> {
let manager = create_manager().context("failed to create cgroup manager")?;
manager.move_to(pid, target_pid)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::io::Write as IoWrite;
#[test]
fn test_cgroup_v1_path_construction() {
let mut mountpoints = HashMap::new();
mountpoints.insert("cpu".to_string(), "/sys/fs/cgroup/cpu".to_string());
mountpoints.insert("memory".to_string(), "/sys/fs/cgroup/memory".to_string());
let result = cgroup_v1_path("cpu", &mountpoints);
assert_eq!(result, Some(PathBuf::from("/sys/fs/cgroup/cpu/cpu/tasks")));
let result = cgroup_v1_path("blkio", &mountpoints);
assert_eq!(result, None);
}
#[test]
fn test_cgroup_v2_path_parses_correctly() {
let temp_dir = std::env::temp_dir().join(format!("cntr_test_{}", std::process::id()));
let pid_dir = temp_dir.join("12345");
fs::create_dir_all(&pid_dir).unwrap();
let cgroup_file = pid_dir.join("cgroup");
let mut file = fs::File::create(&cgroup_file).unwrap();
writeln!(file, "0::/user.slice/user-1000.slice/session-3.scope").unwrap();
let manager = CgroupV2Manager {
mount_path: PathBuf::from("/sys/fs/cgroup"),
procfs_path: temp_dir.clone(),
};
let result = manager
.get_cgroup_path(unistd::Pid::from_raw(12345))
.unwrap();
fs::remove_dir_all(&temp_dir).unwrap();
assert_eq!(
result,
Some("/user.slice/user-1000.slice/session-3.scope".to_string())
);
}
#[test]
fn test_cgroup_v2_path_returns_none_for_v1() {
let temp_dir = std::env::temp_dir().join(format!("cntr_test_v1_{}", std::process::id()));
let pid_dir = temp_dir.join("12346");
fs::create_dir_all(&pid_dir).unwrap();
let cgroup_file = pid_dir.join("cgroup");
let mut file = fs::File::create(&cgroup_file).unwrap();
writeln!(file, "1:name=systemd:/user.slice").unwrap();
writeln!(file, "2:cpu,cpuacct:/user.slice").unwrap();
let manager = CgroupV2Manager {
mount_path: PathBuf::from("/sys/fs/cgroup"),
procfs_path: temp_dir.clone(),
};
let result = manager
.get_cgroup_path(unistd::Pid::from_raw(12346))
.unwrap();
fs::remove_dir_all(&temp_dir).unwrap();
assert_eq!(result, None);
}
}