Skip to main content

diskr/health/
watcher.rs

1use anyhow::Result;
2use colored::Colorize;
3use std::path::PathBuf;
4use std::process::Command;
5use std::time::Duration;
6use tokio::time::interval;
7
8pub struct PostDeleteWatcher {
9    pub removed_paths: Vec<PathBuf>,
10    pub duration: Duration,
11    pub poll_interval: Duration,
12}
13
14impl PostDeleteWatcher {
15    pub fn new(removed_paths: Vec<PathBuf>) -> Self {
16        Self {
17            removed_paths,
18            duration: Duration::from_secs(24 * 3600),
19            poll_interval: Duration::from_secs(300),
20        }
21    }
22
23    pub async fn run(self) -> Result<()> {
24        let elapsed_budget = self.duration;
25        let started = tokio::time::Instant::now();
26        let mut ticker = interval(self.poll_interval);
27        let needles: Vec<String> = self.removed_paths.iter()
28            .filter_map(|p| p.file_name().map(|n| n.to_string_lossy().to_string()))
29            .collect();
30
31        println!("{}", "post-delete watcher started, monitoring for 24h".cyan());
32
33        while started.elapsed() < elapsed_budget {
34            ticker.tick().await;
35            if let Some(hits) = check_journal(&needles).await {
36                for h in hits {
37                    println!("{} {}", "possible fallout (log):".yellow().bold(), h);
38                    println!("  run `diskr restore <name>` to bring it back from trash");
39                }
40            }
41            if let Some(hits) = check_lsof(&needles).await {
42                for h in hits {
43                    println!("{} {}", "possible fallout (open handle):".red().bold(), h);
44                    println!("  a running process still has this file open, deleting it may have broken something");
45                }
46            }
47        }
48        println!("{}", "post-delete watcher finished, no further monitoring".green());
49        Ok(())
50    }
51}
52
53async fn check_journal(needles: &[String]) -> Option<Vec<String>> {
54    if needles.is_empty() {
55        return None;
56    }
57    let output = Command::new("journalctl")
58        .arg("--since").arg("-6min")
59        .arg("-p").arg("err")
60        .arg("--no-pager")
61        .output().ok()?;
62    let text = String::from_utf8_lossy(&output.stdout);
63    let hits: Vec<String> = text.lines()
64        .filter(|line| needles.iter().any(|n| line.contains(n.as_str())))
65        .map(|l| l.to_string())
66        .collect();
67    if hits.is_empty() { None } else { Some(hits) }
68}
69
70async fn check_lsof(needles: &[String]) -> Option<Vec<String>> {
71    if needles.is_empty() {
72        return None;
73    }
74    let output = Command::new("lsof").arg("+L1").output().ok()?;
75    let text = String::from_utf8_lossy(&output.stdout);
76    let hits: Vec<String> = text.lines()
77        .filter(|line| line.contains("(deleted)") && needles.iter().any(|n| line.contains(n.as_str())))
78        .map(|l| l.to_string())
79        .collect();
80    if hits.is_empty() { None } else { Some(hits) }
81}