1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
//! `pelagos cleanup` — remove stale network namespaces, overlay dirs, and DNS temp dirs
//! left behind by containers that exited without proper teardown.
use std::path::Path;
/// Scan for and remove orphaned resources whose owning PID is dead.
///
/// Returns the number of stale entries cleaned.
pub fn cmd_cleanup() -> Result<(), Box<dyn std::error::Error>> {
let mut cleaned = 0u32;
// 1. Stale named network namespaces: /run/netns/rem-{pid}-{n}
cleaned += cleanup_netns()?;
// 2. Stale overlay dirs on the runtime tmpfs (used only in --overlay-tmpfs
// mode): /run/pelagos/overlay-{pid}-{n}/. Cleared on reboot anyway.
cleaned += cleanup_dir_pattern("/run/pelagos", "overlay-")?;
// 2b. Stale overlay dirs on the DISK scratch (the default location):
// <scratch_root>/overlay-{pid}-{n}/. Disk overlays SURVIVE reboot (the
// /run tmpfs did not), so this sweep is what reclaims a leaked/crashed
// container's writable layer. (A PID recycled after reboot can keep one
// stale dir alive — bounded; the common exited-container case is handled.)
let scratch = pelagos::paths::scratch_root();
if let Some(scratch) = scratch.to_str() {
cleaned += cleanup_dir_pattern(scratch, "overlay-")?;
}
// 3. Stale DNS temp dirs: /run/pelagos/dns-{pid}-{n}/
cleaned += cleanup_dir_pattern("/run/pelagos", "dns-")?;
// 4. Stale hosts temp dirs: /run/pelagos/hosts-{pid}-{n}/
cleaned += cleanup_dir_pattern("/run/pelagos", "hosts-")?;
if cleaned == 0 {
println!("No stale resources found.");
} else {
println!("Cleaned {} stale resource(s).", cleaned);
}
Ok(())
}
/// Remove orphaned `/run/netns/rem-*` entries where the owning PID is dead.
fn cleanup_netns() -> Result<u32, Box<dyn std::error::Error>> {
let netns_dir = Path::new("/run/netns");
if !netns_dir.is_dir() {
return Ok(0);
}
let mut count = 0u32;
for entry in std::fs::read_dir(netns_dir)?.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if !name.starts_with("rem-") {
continue;
}
// Parse PID from rem-{pid}-{n}
let pid = match extract_pid(&name, "rem-") {
Some(p) => p,
None => continue,
};
if pid_alive(pid) {
continue;
}
log::info!("removing stale netns: {}", name);
if pelagos::netlink::netns_del(&name).is_ok() {
count += 1;
println!(" removed netns {}", name);
} else {
log::warn!("failed to remove stale netns {}", name);
}
}
Ok(count)
}
/// Remove orphaned `/run/pelagos/{prefix}*` directories where the owning PID is dead.
fn cleanup_dir_pattern(parent: &str, prefix: &str) -> Result<u32, Box<dyn std::error::Error>> {
let parent = Path::new(parent);
if !parent.is_dir() {
return Ok(0);
}
let mut count = 0u32;
for entry in std::fs::read_dir(parent)?.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if !name.starts_with(prefix) {
continue;
}
let pid = match extract_pid(&name, prefix) {
Some(p) => p,
None => continue,
};
if pid_alive(pid) {
continue;
}
let path = entry.path();
if path.is_dir() {
log::info!("removing stale dir: {}", path.display());
if std::fs::remove_dir_all(&path).is_ok() {
count += 1;
println!(" removed {}", path.display());
} else {
log::warn!("failed to remove stale dir {}", path.display());
}
}
}
Ok(count)
}
/// Extract PID from a name like `{prefix}{pid}-{n}`.
fn extract_pid(name: &str, prefix: &str) -> Option<i32> {
let rest = name.strip_prefix(prefix)?;
let pid_str = rest.split('-').next()?;
pid_str.parse::<i32>().ok()
}
/// Check if a PID is still alive.
fn pid_alive(pid: i32) -> bool {
if pid <= 0 {
return false;
}
unsafe { libc::kill(pid, 0) == 0 }
}