Skip to main content

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}