mod_tempdir/cleanup.rs
1//! Orphan-entry cleanup. See [`cleanup_orphans`].
2//!
3//! Companion module to [`crate::TempDir`] and [`crate::NamedTempFile`].
4//! Provides a single free function that scans the OS temp dir for
5//! default-prefix entries this crate could have created and removes
6//! those whose owning processes are no longer alive.
7
8use std::io;
9use std::time::{Duration, SystemTime};
10
11/// Sweep the OS temp directory for default-prefix entries this crate
12/// could have created and remove those that look orphaned.
13///
14/// An entry is removed when **both** of these hold:
15///
16/// 1. **The owning process is not alive.** Each default-prefix entry
17/// carries the originating process's PID in its basename
18/// (`.tmp-{pid}-{name}` for [`crate::TempDir`],
19/// `.tmpfile-{pid}-{name}` for [`crate::NamedTempFile`]). On Linux
20/// liveness is checked via the presence of `/proc/{pid}`. On
21/// macOS and Windows the liveness check is treated as "process is
22/// dead" (since cross-platform process introspection without
23/// platform deps is not available); the age check carries the
24/// safety burden alone on those platforms.
25/// 2. **The entry's mtime is at least `max_age_hours` old.** This is
26/// the load-bearing safety guard on macOS and Windows: pick a
27/// threshold larger than any process you expect to legitimately
28/// hold temp paths.
29///
30/// Entries that do not match the crate's default prefix patterns,
31/// including any caller-supplied `with_prefix(...)` paths, are never
32/// touched. Likewise, legacy entries from `0.9.0` and `0.9.1` that
33/// predate the PID-in-basename format are ignored: they have no
34/// PID segment to parse and are not eligible for cleanup.
35///
36/// # Errors
37///
38/// Returns the underlying [`io::Error`] only if reading the OS temp
39/// directory itself fails. Per-entry failures (permission denied,
40/// entry disappeared between scan and removal, Windows handle still
41/// open, etc.) are intentionally silent and not counted in the
42/// return value. This matches the silent-Drop ethos for the rest of
43/// the crate.
44///
45/// # Returns
46///
47/// The number of entries successfully removed.
48///
49/// # Example
50///
51/// ```no_run
52/// use mod_tempdir::cleanup_orphans;
53///
54/// // At program startup, sweep anything older than 24 hours left
55/// // behind by crashed earlier runs.
56/// let removed = cleanup_orphans(24).unwrap_or(0);
57/// eprintln!("cleanup_orphans removed {removed} orphaned entries");
58/// ```
59pub fn cleanup_orphans(max_age_hours: u64) -> io::Result<usize> {
60 let temp_dir = std::env::temp_dir();
61 let max_age = Duration::from_secs(max_age_hours.saturating_mul(3600));
62 let now = SystemTime::now();
63
64 let mut removed = 0_usize;
65
66 for entry_result in std::fs::read_dir(&temp_dir)? {
67 let entry = match entry_result {
68 Ok(e) => e,
69 Err(_) => continue,
70 };
71
72 if try_remove_one(&entry, now, max_age) {
73 removed += 1;
74 }
75 }
76
77 Ok(removed)
78}
79
80/// Returns `true` iff `entry` was successfully removed.
81///
82/// All non-fatal conditions (wrong prefix, malformed PID, live
83/// process, too recent, removal error) return `false` quietly.
84fn try_remove_one(entry: &std::fs::DirEntry, now: SystemTime, max_age: Duration) -> bool {
85 let name = entry.file_name();
86 let name_str = match name.to_str() {
87 Some(s) => s,
88 None => return false,
89 };
90
91 let (is_dir_pattern, after_prefix) = if let Some(rest) = name_str.strip_prefix(".tmp-") {
92 (true, rest)
93 } else if let Some(rest) = name_str.strip_prefix(".tmpfile-") {
94 (false, rest)
95 } else {
96 return false;
97 };
98
99 // Parse `{digits}-`. Legacy entries (no PID segment) fall out
100 // here and are left alone.
101 let (pid_str, _name12) = match after_prefix.split_once('-') {
102 Some(p) => p,
103 None => return false,
104 };
105 let pid = match pid_str.parse::<u32>() {
106 Ok(p) => p,
107 Err(_) => return false,
108 };
109
110 if pid_alive(pid) {
111 return false;
112 }
113
114 let metadata = match entry.metadata() {
115 Ok(m) => m,
116 Err(_) => return false,
117 };
118 let mtime = match metadata.modified() {
119 Ok(t) => t,
120 Err(_) => return false,
121 };
122 let age = match now.duration_since(mtime) {
123 Ok(d) => d,
124 Err(_) => return false,
125 };
126 if age < max_age {
127 return false;
128 }
129
130 let path = entry.path();
131 let result = if is_dir_pattern {
132 std::fs::remove_dir_all(&path)
133 } else {
134 std::fs::remove_file(&path)
135 };
136 result.is_ok()
137}
138
139/// Returns `true` if process `pid` is alive on this host.
140///
141/// Linux: checks `/proc/{pid}` for existence.
142///
143/// macOS, Windows: returns `false` unconditionally. Cross-platform
144/// process introspection without `libc` / `windows-sys` is not
145/// available; the age check is the sole safety gate on those
146/// platforms. Returning `false` here lets the AND condition in
147/// [`cleanup_orphans`] degrade to age-only.
148#[cfg(target_os = "linux")]
149fn pid_alive(pid: u32) -> bool {
150 std::path::Path::new(&format!("/proc/{pid}")).exists()
151}
152
153#[cfg(not(target_os = "linux"))]
154fn pid_alive(_pid: u32) -> bool {
155 // Cross-platform process introspection without deps is not
156 // available; see [`cleanup_orphans`] rustdoc. Returning false
157 // lets the AND condition degrade to age-only on this platform.
158 false
159}