browser_automation_cli/
residual.rs1use std::path::{Path, PathBuf};
10use std::time::{Duration, SystemTime};
11
12pub fn discover_owned_chromium_tmp_side_channels(
15 profile: Option<&Path>,
16 chrome_pid: Option<u32>,
17 not_before: SystemTime,
18) -> Vec<PathBuf> {
19 let mut out = Vec::new();
20 let tmp = std::env::temp_dir();
21 let Ok(entries) = std::fs::read_dir(&tmp) else {
22 return out;
23 };
24 let pid_s = chrome_pid.map(|p| p.to_string());
25 let profile_s = profile.map(|p| p.display().to_string());
26
27 for ent in entries.flatten() {
28 let name = ent.file_name();
29 let name = name.to_string_lossy();
30 let is_chromium_tmp = name.starts_with("org.chromium.Chromium.")
31 || name.starts_with(".org.chromium.Chromium.")
32 || name.starts_with(".com.google.Chrome.")
33 || name.starts_with("com.google.Chrome.");
34 let is_cli_marker = name.starts_with("browser-automation-cli-chrome-");
35 if !is_chromium_tmp && !is_cli_marker {
36 continue;
37 }
38 let path = ent.path();
39 if !owned_by_current_user(&path) {
40 continue;
41 }
42 if !created_or_modified_after(&path, not_before) {
43 continue;
44 }
45 if is_cli_marker {
46 out.push(path);
48 continue;
49 }
50 if let Some(ref pid) = pid_s {
52 if path_references(&path, pid) {
53 out.push(path);
54 continue;
55 }
56 }
57 if let Some(ref prof) = profile_s {
58 if path_references(&path, prof) {
59 out.push(path);
60 continue;
61 }
62 }
63 if age_since(not_before) < Duration::from_secs(5) {
66 if let Ok(meta) = path.metadata() {
68 if meta.len() <= 4096 {
69 out.push(path);
70 }
71 }
72 }
73 }
74 out
75}
76
77pub fn list_cli_chrome_marker_dirs() -> Vec<PathBuf> {
79 let mut out = Vec::new();
80 let tmp = std::env::temp_dir();
81 let Ok(entries) = std::fs::read_dir(tmp) else {
82 return out;
83 };
84 for ent in entries.flatten() {
85 let name = ent.file_name();
86 let name = name.to_string_lossy();
87 if name.starts_with("browser-automation-cli-chrome-") {
88 out.push(ent.path());
89 }
90 }
91 out
92}
93
94pub fn scavenge_owned_chromium_tmp_orphans(
100 profile: Option<&Path>,
101 chrome_pid: Option<u32>,
102 not_before: SystemTime,
103) -> Vec<PathBuf> {
104 let candidates = discover_owned_chromium_tmp_side_channels(profile, chrome_pid, not_before);
105 let mut wiped = Vec::new();
106 for path in candidates {
107 if path_has_live_process(&path) {
108 continue;
109 }
110 let name = path
112 .file_name()
113 .map(|s| s.to_string_lossy().into_owned())
114 .unwrap_or_default();
115 let is_cli_marker = name.starts_with("browser-automation-cli-chrome-");
116 let is_singletonish = is_singleton_only_or_small(&path);
117 if is_cli_marker || is_singletonish {
118 wipe_path(&path);
119 wiped.push(path);
120 }
121 }
122 wiped
123}
124
125fn is_singleton_only_or_small(path: &Path) -> bool {
126 if !path.is_dir() {
127 if let Ok(meta) = path.metadata() {
128 return meta.len() <= 4096;
129 }
130 return false;
131 }
132 let Ok(entries) = std::fs::read_dir(path) else {
133 return false;
134 };
135 let mut count = 0usize;
136 let mut only_singleton = true;
137 for ent in entries.flatten() {
138 count += 1;
139 if count > 8 {
140 return false;
141 }
142 let n = ent.file_name();
143 let n = n.to_string_lossy();
144 if !(n.starts_with("Singleton")
145 || n == "DevToolsActivePort"
146 || n.starts_with(".org.chromium")
147 || n.ends_with(".lock"))
148 {
149 only_singleton = false;
150 }
151 }
152 only_singleton || count == 0
153}
154
155fn path_has_live_process(path: &Path) -> bool {
156 let needle = path.display().to_string();
157 #[cfg(unix)]
158 {
159 let Ok(proc) = std::fs::read_dir("/proc") else {
161 return false;
162 };
163 for ent in proc.flatten() {
164 let name = ent.file_name();
165 let name = name.to_string_lossy();
166 if !name.chars().all(|c| c.is_ascii_digit()) {
167 continue;
168 }
169 let cmdline = ent.path().join("cmdline");
170 if let Ok(bytes) = std::fs::read(cmdline) {
171 let s = String::from_utf8_lossy(&bytes);
172 if s.contains(&needle) {
173 return true;
174 }
175 }
176 }
177 false
178 }
179 #[cfg(not(unix))]
180 {
181 let _ = needle;
182 false
183 }
184}
185
186fn wipe_path(path: &Path) {
187 if !path.exists() {
188 return;
189 }
190 if path.is_dir() {
191 let _ = std::fs::remove_dir_all(path);
192 } else {
193 let _ = std::fs::remove_file(path);
194 }
195}
196
197fn owned_by_current_user(path: &Path) -> bool {
198 #[cfg(unix)]
199 {
200 use std::os::unix::fs::MetadataExt;
201 let Ok(meta) = path.metadata() else {
202 return false;
203 };
204 meta.uid() == unsafe { libc::getuid() }
205 }
206 #[cfg(not(unix))]
207 {
208 let _ = path;
209 true
210 }
211}
212
213fn created_or_modified_after(path: &Path, not_before: SystemTime) -> bool {
214 let Ok(meta) = path.metadata() else {
215 return false;
216 };
217 let modified = meta.modified().ok();
218 let created = meta.created().ok();
219 let skew = Duration::from_secs(2);
220 let threshold = not_before.checked_sub(skew).unwrap_or(not_before);
221 if let Some(m) = modified {
222 if m >= threshold {
223 return true;
224 }
225 }
226 if let Some(c) = created {
227 if c >= threshold {
228 return true;
229 }
230 }
231 false
232}
233
234fn path_references(path: &Path, needle: &str) -> bool {
235 if path.display().to_string().contains(needle) {
236 return true;
237 }
238 if let Ok(target) = std::fs::read_link(path) {
240 if target.to_string_lossy().contains(needle) {
241 return true;
242 }
243 }
244 if let Ok(meta) = path.metadata() {
246 if meta.is_file() && meta.len() <= 4096 {
247 if let Ok(bytes) = std::fs::read(path) {
248 if String::from_utf8_lossy(&bytes).contains(needle) {
249 return true;
250 }
251 }
252 }
253 }
254 false
255}
256
257fn age_since(t: SystemTime) -> Duration {
258 SystemTime::now().duration_since(t).unwrap_or_default()
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use std::fs;
265 use std::time::SystemTime;
266
267 #[test]
268 fn marker_scan_finds_and_filters() {
269 let tmp = std::env::temp_dir();
270 let dir = tmp.join(format!(
271 "browser-automation-cli-chrome-test-{}",
272 uuid::Uuid::new_v4()
273 ));
274 let _ = fs::create_dir_all(&dir);
275 let found = list_cli_chrome_marker_dirs();
276 assert!(
277 found.iter().any(|p| p == &dir),
278 "expected marker dir in list"
279 );
280 let _ = fs::remove_dir_all(&dir);
281 }
282
283 #[test]
284 fn discover_respects_not_before() {
285 let future = SystemTime::now() + Duration::from_secs(3600);
286 let found = discover_owned_chromium_tmp_side_channels(None, None, future);
287 assert!(
288 found.is_empty(),
289 "future not_before must yield no side channels"
290 );
291 }
292}