1use std::path::Path;
8
9#[derive(Copy, Clone, Default, Debug, PartialEq, Eq)]
11pub struct SmapsRollup {
12 pub rss: u64,
13 pub pss: u64,
14 pub pss_anon: u64,
15 pub pss_file: u64,
16 pub pss_shmem: u64,
17 pub private_clean: u64,
18 pub private_dirty: u64,
19 pub swap: u64,
20 pub swap_pss: u64,
21}
22
23impl SmapsRollup {
24 pub fn epss(&self) -> u64 {
26 self.pss.saturating_add(self.swap_pss)
27 }
28
29 pub fn private(&self) -> u64 {
31 self.private_clean.saturating_add(self.private_dirty)
32 }
33}
34
35pub fn parse_smaps_rollup(buf: &[u8]) -> Option<SmapsRollup> {
37 let mut out = SmapsRollup::default();
38 let mut seen = false;
39
40 for line in buf.split(|b| *b == b'\n') {
41 let Some(colon) = memchr::memchr(b':', line) else {
42 continue;
43 };
44 let key = &line[..colon];
45 let rest = &line[colon + 1..];
46 let Some(kb) = first_number(rest) else {
47 continue;
48 };
49 let bytes = kb.saturating_mul(1024);
50
51 match key {
52 b"Rss" => out.rss = bytes,
53 b"Pss" => out.pss = bytes,
54 b"Pss_Anon" => out.pss_anon = bytes,
55 b"Pss_File" => out.pss_file = bytes,
56 b"Pss_Shmem" => out.pss_shmem = bytes,
57 b"Private_Clean" => out.private_clean = bytes,
58 b"Private_Dirty" => out.private_dirty = bytes,
59 b"Swap" => out.swap = bytes,
60 b"SwapPss" => out.swap_pss = bytes,
61 _ => continue,
62 }
63 seen = true;
64 }
65
66 if seen {
67 Some(out)
68 } else {
69 None
70 }
71}
72
73fn first_number(buf: &[u8]) -> Option<u64> {
74 let start = buf.iter().position(|c| c.is_ascii_digit())?;
75 let mut v: u64 = 0;
76 for &c in &buf[start..] {
77 if !c.is_ascii_digit() {
78 break;
79 }
80 v = v.wrapping_mul(10).wrapping_add((c - b'0') as u64);
81 }
82 Some(v)
83}
84
85#[derive(Clone, Debug, PartialEq, Eq)]
87pub struct CgroupEntry {
88 pub hierarchy: u32,
89 pub controllers: String,
90 pub path: String,
91}
92
93pub fn parse_cgroup(buf: &[u8]) -> Vec<CgroupEntry> {
98 let mut out = Vec::new();
99 for line in buf.split(|b| *b == b'\n') {
100 let line = trim(line);
101 if line.is_empty() {
102 continue;
103 }
104 let mut parts = line.splitn(3, |b| *b == b':');
105 let (Some(hier), Some(ctrls), Some(path)) = (parts.next(), parts.next(), parts.next())
106 else {
107 continue;
108 };
109 let hierarchy = std::str::from_utf8(hier)
110 .ok()
111 .and_then(|s| s.parse().ok())
112 .unwrap_or(0);
113 out.push(CgroupEntry {
114 hierarchy,
115 controllers: String::from_utf8_lossy(ctrls).into_owned(),
116 path: String::from_utf8_lossy(path).into_owned(),
117 });
118 }
119 out
120}
121
122pub fn preferred_path(entries: &[CgroupEntry]) -> Option<&str> {
129 if let Some(unified) = entries
131 .iter()
132 .find(|e| e.hierarchy == 0 && e.controllers.is_empty())
133 {
134 return Some(&unified.path);
135 }
136 entries.first().map(|e| e.path.as_str())
137}
138
139#[derive(Clone, Debug, PartialEq, Eq)]
147pub struct Container {
148 pub runtime: &'static str,
150 pub id: String,
156}
157
158impl Container {
159 pub fn short_id(&self) -> &str {
166 let end = self.id.len().min(12);
167 &self.id[..end]
169 }
170}
171
172pub fn container_from_cgroup(entries: &[CgroupEntry]) -> Option<Container> {
189 for entry in entries {
190 if let Some(c) = container_from_path(&entry.path) {
191 return Some(c);
192 }
193 }
194 None
195}
196
197pub fn container_from_path(path: &str) -> Option<Container> {
202 if let Some(rest) = path.strip_prefix("/kubepods") {
206 return Some(Container {
207 runtime: "kubepods",
208 id: kubernetes_id(rest),
209 });
210 }
211
212 if let Some(rest) = path.strip_prefix("/docker/") {
215 return Some(Container {
216 runtime: "docker",
217 id: trim_scope(rest).to_string(),
218 });
219 }
220 if let Some(pos) = path.find("docker-") {
221 let rest = &path[pos + "docker-".len()..];
223 return Some(Container {
224 runtime: "docker",
225 id: trim_scope(rest).to_string(),
226 });
227 }
228
229 if let Some(pos) = path.find("libpod-") {
230 let rest = &path[pos + "libpod-".len()..];
231 return Some(Container {
232 runtime: "podman",
233 id: trim_scope(rest).to_string(),
234 });
235 }
236 if let Some(pos) = path.find("podman-") {
238 let rest = &path[pos + "podman-".len()..];
239 return Some(Container {
240 runtime: "podman",
241 id: trim_scope(rest).to_string(),
242 });
243 }
244
245 if let Some(pos) = path.find("lxc.payload.") {
246 let rest = &path[pos + "lxc.payload.".len()..];
247 return Some(Container {
248 runtime: "lxc",
249 id: rest.trim_end_matches('/').to_string(),
250 });
251 }
252
253 if let Some(pos) = path.find("machine-") {
256 let rest = &path[pos + "machine-".len()..];
257 return Some(Container {
258 runtime: "nspawn",
259 id: trim_scope(rest).to_string(),
260 });
261 }
262
263 None
267}
268
269fn trim_scope(s: &str) -> &str {
271 let s = s.trim_end_matches('/');
272 s.strip_suffix(".scope")
273 .or_else(|| s.strip_suffix(".slice"))
274 .unwrap_or(s)
275}
276
277pub fn self_pid_namespace(proc_root: &Path) -> Option<u64> {
290 use std::os::unix::fs::MetadataExt as _;
291 std::fs::metadata(proc_root.join("self/ns/pid"))
292 .ok()
293 .map(|m| m.ino())
294}
295
296pub fn in_different_pid_namespace(proc_root: &Path, pid: crate::Pid) -> Option<bool> {
314 use std::os::unix::fs::MetadataExt as _;
315 let mine = std::fs::metadata(proc_root.join(pid.to_string()).join("ns/pid"))
316 .ok()?
317 .ino();
318 let ours = self_pid_namespace(proc_root)?;
319 Some(mine != ours)
320}
321
322fn kubernetes_id(rest: &str) -> String {
328 if let Some(pos) = rest.find("-pod") {
331 let after = &rest[pos + "-pod".len()..];
332 let end = after
333 .find(['.', '_', '/'])
334 .unwrap_or(after.len());
335 let uid = &after[..end];
336 if !uid.is_empty() {
337 return uid.to_string();
338 }
339 }
340 rest.trim_start_matches('/')
342 .split('/')
343 .next()
344 .map(trim_scope)
345 .unwrap_or("")
346 .to_string()
347}
348
349fn trim(mut b: &[u8]) -> &[u8] {
350 while let Some((first, rest)) = b.split_first() {
351 if first.is_ascii_whitespace() {
352 b = rest;
353 } else {
354 break;
355 }
356 }
357 while let Some((last, rest)) = b.split_last() {
358 if last.is_ascii_whitespace() {
359 b = rest;
360 } else {
361 break;
362 }
363 }
364 b
365}
366
367pub fn smaps_rollup_available(proc_root: &Path, pid: crate::Pid) -> bool {
369 proc_root
370 .join(pid.to_string())
371 .join("smaps_rollup")
372 .exists()
373}
374
375pub fn read_smaps_rollup(proc_root: &Path, pid: crate::Pid) -> Option<SmapsRollup> {
389 let path = proc_root.join(pid.to_string()).join("smaps_rollup");
390 let bytes = std::fs::read(path).ok()?;
391 parse_smaps_rollup(&bytes)
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397
398 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";
399
400 #[test]
401 fn smaps_rollup_parsing() {
402 let r = parse_smaps_rollup(SMAPS).expect("must parse");
403 assert_eq!(r.rss, 4800 * 1024);
404 assert_eq!(r.pss, 2400 * 1024);
405 assert_eq!(r.swap_pss, 50 * 1024);
406 assert_eq!(r.epss(), (2400 + 50) * 1024);
407 assert_eq!(r.private(), (400 + 900) * 1024);
408 }
409
410 #[test]
411 fn smaps_rollup_rejects_garbage() {
412 assert!(parse_smaps_rollup(b"hello world\n").is_none());
413 }
414
415 #[test]
416 fn cgroup_v2() {
417 let e = parse_cgroup(b"0::/user.slice/user-1000.slice/session-1.scope\n");
418 assert_eq!(e.len(), 1);
419 assert_eq!(e[0].hierarchy, 0);
420 assert!(e[0].controllers.is_empty());
421 assert_eq!(e[0].path, "/user.slice/user-1000.slice/session-1.scope");
422 }
423
424 #[test]
425 fn cgroup_v1_multi() {
426 let buf =
427 b"12:cpuset:/docker/abc123\n5:memory:/docker/abc123\n1:name=systemd:/user.slice/x\n";
428 let e = parse_cgroup(buf);
429 assert_eq!(e.len(), 3);
430 let c = container_from_cgroup(&e).expect("docker");
431 assert_eq!(c.runtime, "docker");
432 assert_eq!(c.id, "abc123");
433 }
434
435 #[test]
436 fn no_container_for_plain_host_process() {
437 let e = parse_cgroup(b"0::/user.slice/session-2.scope\n");
438 assert_eq!(container_from_cgroup(&e), None);
439 }
440
441 #[test]
447 fn docker_paths_yield_the_container_id() {
448 let c = container_from_path("/docker/1f2e3d4c5b6a7988").expect("docker");
450 assert_eq!(c.runtime, "docker");
451 assert_eq!(c.id, "1f2e3d4c5b6a7988");
452
453 let c = container_from_path("/system.slice/docker-1f2e3d4c5b6a7988.scope").expect("docker");
456 assert_eq!(c.runtime, "docker");
457 assert_eq!(c.id, "1f2e3d4c5b6a7988");
458 }
459
460 #[test]
461 fn podman_paths_are_recognised() {
462 let c = container_from_path("/machine.slice/libpod-abc123def456.scope").expect("podman");
463 assert_eq!(c.runtime, "podman");
464 assert_eq!(c.id, "abc123def456");
465
466 let c = container_from_path("/user.slice/podman-999.scope").expect("podman");
467 assert_eq!(c.runtime, "podman");
468 assert_eq!(c.id, "999");
469 }
470
471 #[test]
472 fn lxc_and_nspawn_paths_are_recognised() {
473 let c = container_from_path("/lxc.payload.web-1").expect("lxc");
474 assert_eq!(c.runtime, "lxc");
475 assert_eq!(c.id, "web-1");
476
477 let c = container_from_path("/machine.slice/machine-build.scope").expect("nspawn");
478 assert_eq!(c.runtime, "nspawn");
479 assert_eq!(c.id, "build");
480 }
481
482 #[test]
485 fn kubernetes_paths_yield_the_pod_uid() {
486 let path = "/kubepods.slice/kubepods-burstable.slice/\
487 kubepods-burstable-pod1a2b3c4d-5e6f-7890-abcd-ef1234567890.slice/\
488 cri-containerd-9f8e7d6c5b4a3210.scope";
489 let c = container_from_path(path).expect("kubepods");
490 assert_eq!(c.runtime, "kubepods");
491 assert_eq!(c.id, "1a2b3c4d-5e6f-7890-abcd-ef1234567890");
492 }
493
494 #[test]
495 fn kubernetes_wins_over_the_containerd_leaf() {
496 let path = "/kubepods.slice/kubepods-besteffort.slice/\
499 kubepods-besteffort-podabcd.slice/cri-containerd-1234.scope";
500 let c = container_from_path(path).expect("a container");
501 assert_eq!(c.runtime, "kubepods");
502 assert_eq!(c.id, "abcd");
503 }
504
505 #[test]
508 fn the_host_daemon_is_not_a_container() {
509 assert_eq!(container_from_path("/system.slice/containerd.service"), None);
510 assert_eq!(container_from_path("/system.slice/docker.service"), None);
511 assert_eq!(container_from_path("/"), None);
512 assert_eq!(container_from_path("/init.scope"), None);
513 assert_eq!(container_from_path("/user.slice/user-1000.slice/session-1.scope"), None);
514 }
515
516 #[test]
517 fn short_id_is_twelve_characters() {
518 let c = Container {
519 runtime: "docker",
520 id: "1f2e3d4c5b6a79880a1b2c3d4e5f6071".to_string(),
521 };
522 assert_eq!(c.short_id(), "1f2e3d4c5b6a");
523
524 let c = Container {
526 runtime: "lxc",
527 id: "web-1".to_string(),
528 };
529 assert_eq!(c.short_id(), "web-1");
530
531 let c = Container {
532 runtime: "kubepods",
533 id: String::new(),
534 };
535 assert_eq!(c.short_id(), "");
536 }
537}