use std::path::Path;
#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
pub struct SmapsRollup {
pub rss: u64,
pub pss: u64,
pub pss_anon: u64,
pub pss_file: u64,
pub pss_shmem: u64,
pub private_clean: u64,
pub private_dirty: u64,
pub swap: u64,
pub swap_pss: u64,
}
impl SmapsRollup {
pub fn epss(&self) -> u64 {
self.pss.saturating_add(self.swap_pss)
}
pub fn private(&self) -> u64 {
self.private_clean.saturating_add(self.private_dirty)
}
}
pub fn parse_smaps_rollup(buf: &[u8]) -> Option<SmapsRollup> {
let mut out = SmapsRollup::default();
let mut seen = false;
for line in buf.split(|b| *b == b'\n') {
let Some(colon) = memchr::memchr(b':', line) else {
continue;
};
let key = &line[..colon];
let rest = &line[colon + 1..];
let Some(kb) = first_number(rest) else {
continue;
};
let bytes = kb.saturating_mul(1024);
match key {
b"Rss" => out.rss = bytes,
b"Pss" => out.pss = bytes,
b"Pss_Anon" => out.pss_anon = bytes,
b"Pss_File" => out.pss_file = bytes,
b"Pss_Shmem" => out.pss_shmem = bytes,
b"Private_Clean" => out.private_clean = bytes,
b"Private_Dirty" => out.private_dirty = bytes,
b"Swap" => out.swap = bytes,
b"SwapPss" => out.swap_pss = bytes,
_ => continue,
}
seen = true;
}
if seen {
Some(out)
} else {
None
}
}
fn first_number(buf: &[u8]) -> Option<u64> {
let start = buf.iter().position(|c| c.is_ascii_digit())?;
let mut v: u64 = 0;
for &c in &buf[start..] {
if !c.is_ascii_digit() {
break;
}
v = v.wrapping_mul(10).wrapping_add((c - b'0') as u64);
}
Some(v)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CgroupEntry {
pub hierarchy: u32,
pub controllers: String,
pub path: String,
}
pub fn parse_cgroup(buf: &[u8]) -> Vec<CgroupEntry> {
let mut out = Vec::new();
for line in buf.split(|b| *b == b'\n') {
let line = trim(line);
if line.is_empty() {
continue;
}
let mut parts = line.splitn(3, |b| *b == b':');
let (Some(hier), Some(ctrls), Some(path)) = (parts.next(), parts.next(), parts.next())
else {
continue;
};
let hierarchy = std::str::from_utf8(hier)
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(0);
out.push(CgroupEntry {
hierarchy,
controllers: String::from_utf8_lossy(ctrls).into_owned(),
path: String::from_utf8_lossy(path).into_owned(),
});
}
out
}
pub fn preferred_path(entries: &[CgroupEntry]) -> Option<&str> {
if let Some(unified) = entries
.iter()
.find(|e| e.hierarchy == 0 && e.controllers.is_empty())
{
return Some(&unified.path);
}
entries.first().map(|e| e.path.as_str())
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Container {
pub runtime: &'static str,
pub id: String,
}
impl Container {
pub fn short_id(&self) -> &str {
let end = self.id.len().min(12);
&self.id[..end]
}
}
pub fn container_from_cgroup(entries: &[CgroupEntry]) -> Option<Container> {
for entry in entries {
if let Some(c) = container_from_path(&entry.path) {
return Some(c);
}
}
None
}
pub fn container_from_path(path: &str) -> Option<Container> {
if let Some(rest) = path.strip_prefix("/kubepods") {
return Some(Container {
runtime: "kubepods",
id: kubernetes_id(rest),
});
}
if let Some(rest) = path.strip_prefix("/docker/") {
return Some(Container {
runtime: "docker",
id: trim_scope(rest).to_string(),
});
}
if let Some(pos) = path.find("docker-") {
let rest = &path[pos + "docker-".len()..];
return Some(Container {
runtime: "docker",
id: trim_scope(rest).to_string(),
});
}
if let Some(pos) = path.find("libpod-") {
let rest = &path[pos + "libpod-".len()..];
return Some(Container {
runtime: "podman",
id: trim_scope(rest).to_string(),
});
}
if let Some(pos) = path.find("podman-") {
let rest = &path[pos + "podman-".len()..];
return Some(Container {
runtime: "podman",
id: trim_scope(rest).to_string(),
});
}
if let Some(pos) = path.find("lxc.payload.") {
let rest = &path[pos + "lxc.payload.".len()..];
return Some(Container {
runtime: "lxc",
id: rest.trim_end_matches('/').to_string(),
});
}
if let Some(pos) = path.find("machine-") {
let rest = &path[pos + "machine-".len()..];
return Some(Container {
runtime: "nspawn",
id: trim_scope(rest).to_string(),
});
}
None
}
fn trim_scope(s: &str) -> &str {
let s = s.trim_end_matches('/');
s.strip_suffix(".scope")
.or_else(|| s.strip_suffix(".slice"))
.unwrap_or(s)
}
pub fn self_pid_namespace(proc_root: &Path) -> Option<u64> {
use std::os::unix::fs::MetadataExt as _;
std::fs::metadata(proc_root.join("self/ns/pid"))
.ok()
.map(|m| m.ino())
}
pub fn in_different_pid_namespace(proc_root: &Path, pid: crate::Pid) -> Option<bool> {
use std::os::unix::fs::MetadataExt as _;
let mine = std::fs::metadata(proc_root.join(pid.to_string()).join("ns/pid"))
.ok()?
.ino();
let ours = self_pid_namespace(proc_root)?;
Some(mine != ours)
}
fn kubernetes_id(rest: &str) -> String {
if let Some(pos) = rest.find("-pod") {
let after = &rest[pos + "-pod".len()..];
let end = after
.find(['.', '_', '/'])
.unwrap_or(after.len());
let uid = &after[..end];
if !uid.is_empty() {
return uid.to_string();
}
}
rest.trim_start_matches('/')
.split('/')
.next()
.map(trim_scope)
.unwrap_or("")
.to_string()
}
fn trim(mut b: &[u8]) -> &[u8] {
while let Some((first, rest)) = b.split_first() {
if first.is_ascii_whitespace() {
b = rest;
} else {
break;
}
}
while let Some((last, rest)) = b.split_last() {
if last.is_ascii_whitespace() {
b = rest;
} else {
break;
}
}
b
}
pub fn smaps_rollup_available(proc_root: &Path, pid: crate::Pid) -> bool {
proc_root
.join(pid.to_string())
.join("smaps_rollup")
.exists()
}
pub fn read_smaps_rollup(proc_root: &Path, pid: crate::Pid) -> Option<SmapsRollup> {
let path = proc_root.join(pid.to_string()).join("smaps_rollup");
let bytes = std::fs::read(path).ok()?;
parse_smaps_rollup(&bytes)
}
#[cfg(test)]
mod tests {
use super::*;
const SMAPS: &[u8] = b"55d0e0000000-7ffe00000000 ---p 00000000 00:00 0 [rollup]\nRss: 4800 kB\nPss: 2400 kB\nPss_Anon: 1600 kB\nPss_File: 500 kB\nPss_Shmem: 300 kB\nPrivate_Clean: 400 kB\nPrivate_Dirty: 900 kB\nSwap: 100 kB\nSwapPss: 50 kB\n";
#[test]
fn smaps_rollup_parsing() {
let r = parse_smaps_rollup(SMAPS).expect("must parse");
assert_eq!(r.rss, 4800 * 1024);
assert_eq!(r.pss, 2400 * 1024);
assert_eq!(r.swap_pss, 50 * 1024);
assert_eq!(r.epss(), (2400 + 50) * 1024);
assert_eq!(r.private(), (400 + 900) * 1024);
}
#[test]
fn smaps_rollup_rejects_garbage() {
assert!(parse_smaps_rollup(b"hello world\n").is_none());
}
#[test]
fn cgroup_v2() {
let e = parse_cgroup(b"0::/user.slice/user-1000.slice/session-1.scope\n");
assert_eq!(e.len(), 1);
assert_eq!(e[0].hierarchy, 0);
assert!(e[0].controllers.is_empty());
assert_eq!(e[0].path, "/user.slice/user-1000.slice/session-1.scope");
}
#[test]
fn cgroup_v1_multi() {
let buf =
b"12:cpuset:/docker/abc123\n5:memory:/docker/abc123\n1:name=systemd:/user.slice/x\n";
let e = parse_cgroup(buf);
assert_eq!(e.len(), 3);
let c = container_from_cgroup(&e).expect("docker");
assert_eq!(c.runtime, "docker");
assert_eq!(c.id, "abc123");
}
#[test]
fn no_container_for_plain_host_process() {
let e = parse_cgroup(b"0::/user.slice/session-2.scope\n");
assert_eq!(container_from_cgroup(&e), None);
}
#[test]
fn docker_paths_yield_the_container_id() {
let c = container_from_path("/docker/1f2e3d4c5b6a7988").expect("docker");
assert_eq!(c.runtime, "docker");
assert_eq!(c.id, "1f2e3d4c5b6a7988");
let c = container_from_path("/system.slice/docker-1f2e3d4c5b6a7988.scope").expect("docker");
assert_eq!(c.runtime, "docker");
assert_eq!(c.id, "1f2e3d4c5b6a7988");
}
#[test]
fn podman_paths_are_recognised() {
let c = container_from_path("/machine.slice/libpod-abc123def456.scope").expect("podman");
assert_eq!(c.runtime, "podman");
assert_eq!(c.id, "abc123def456");
let c = container_from_path("/user.slice/podman-999.scope").expect("podman");
assert_eq!(c.runtime, "podman");
assert_eq!(c.id, "999");
}
#[test]
fn lxc_and_nspawn_paths_are_recognised() {
let c = container_from_path("/lxc.payload.web-1").expect("lxc");
assert_eq!(c.runtime, "lxc");
assert_eq!(c.id, "web-1");
let c = container_from_path("/machine.slice/machine-build.scope").expect("nspawn");
assert_eq!(c.runtime, "nspawn");
assert_eq!(c.id, "build");
}
#[test]
fn kubernetes_paths_yield_the_pod_uid() {
let path = "/kubepods.slice/kubepods-burstable.slice/\
kubepods-burstable-pod1a2b3c4d-5e6f-7890-abcd-ef1234567890.slice/\
cri-containerd-9f8e7d6c5b4a3210.scope";
let c = container_from_path(path).expect("kubepods");
assert_eq!(c.runtime, "kubepods");
assert_eq!(c.id, "1a2b3c4d-5e6f-7890-abcd-ef1234567890");
}
#[test]
fn kubernetes_wins_over_the_containerd_leaf() {
let path = "/kubepods.slice/kubepods-besteffort.slice/\
kubepods-besteffort-podabcd.slice/cri-containerd-1234.scope";
let c = container_from_path(path).expect("a container");
assert_eq!(c.runtime, "kubepods");
assert_eq!(c.id, "abcd");
}
#[test]
fn the_host_daemon_is_not_a_container() {
assert_eq!(container_from_path("/system.slice/containerd.service"), None);
assert_eq!(container_from_path("/system.slice/docker.service"), None);
assert_eq!(container_from_path("/"), None);
assert_eq!(container_from_path("/init.scope"), None);
assert_eq!(container_from_path("/user.slice/user-1000.slice/session-1.scope"), None);
}
#[test]
fn short_id_is_twelve_characters() {
let c = Container {
runtime: "docker",
id: "1f2e3d4c5b6a79880a1b2c3d4e5f6071".to_string(),
};
assert_eq!(c.short_id(), "1f2e3d4c5b6a");
let c = Container {
runtime: "lxc",
id: "web-1".to_string(),
};
assert_eq!(c.short_id(), "web-1");
let c = Container {
runtime: "kubepods",
id: String::new(),
};
assert_eq!(c.short_id(), "");
}
}