Skip to main content

running_process/cleanup/
verify_basic.rs

1use std::path::Path;
2
3use crate::broker::{host_identity, manifest};
4use crate::cleanup::json_escape;
5
6/// One manifest verification finding.
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct VerifyFinding {
9    /// Manifest path.
10    pub path: std::path::PathBuf,
11    /// Finding severity.
12    pub severity: &'static str,
13    /// Human-readable message.
14    pub message: String,
15}
16
17/// Basic v1 verification result.
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct VerifyReport {
20    /// Number of `.pb` entries scanned.
21    pub scanned: usize,
22    /// Findings generated during verification.
23    pub findings: Vec<VerifyFinding>,
24}
25
26/// Run basic verification over the central registry.
27pub fn run(registry_dir: &Path) -> VerifyReport {
28    let current = host_identity::current();
29    let entries = manifest::scan_central(registry_dir);
30    let mut findings = Vec::new();
31    let scanned = entries.len();
32
33    for entry in entries {
34        match entry.result {
35            Ok(manifest) => {
36                if let Some(host) = manifest.host.as_ref() {
37                    if !host.machine_id.is_empty() && host.machine_id != current.machine_id {
38                        findings.push(VerifyFinding {
39                            path: entry.path.clone(),
40                            severity: "stale",
41                            message: "manifest belongs to another machine".to_string(),
42                        });
43                    }
44                    if !host.boot_id.is_empty() && host.boot_id != current.boot_id {
45                        findings.push(VerifyFinding {
46                            path: entry.path.clone(),
47                            severity: "stale",
48                            message: "manifest belongs to a prior boot".to_string(),
49                        });
50                    }
51                }
52                if let Some(daemon) = manifest.current_daemon.as_ref() {
53                    if !process_is_alive(daemon.pid) {
54                        findings.push(VerifyFinding {
55                            path: entry.path,
56                            severity: "stale",
57                            message: format!("daemon pid {} is not alive", daemon.pid),
58                        });
59                    }
60                }
61            }
62            Err(err) => findings.push(VerifyFinding {
63                path: entry.path,
64                severity: "error",
65                message: err.to_string(),
66            }),
67        }
68    }
69
70    VerifyReport { scanned, findings }
71}
72
73/// Render `running-process-cleanup verify --json`.
74pub fn render_json(report: &VerifyReport) -> String {
75    let findings = report
76        .findings
77        .iter()
78        .map(|finding| {
79            format!(
80                "{{\"path\":\"{}\",\"severity\":\"{}\",\"message\":\"{}\"}}",
81                json_escape(&finding.path.to_string_lossy()),
82                finding.severity,
83                json_escape(&finding.message)
84            )
85        })
86        .collect::<Vec<_>>()
87        .join(",");
88    format!(
89        "{{\"schema_version\":1,\"scanned\":{},\"findings\":[{}]}}",
90        report.scanned, findings
91    )
92}
93
94#[cfg(test)]
95#[path = "../tests/verify_basic_coverage.rs"]
96mod coverage_tests;
97
98/// Whether a PID still names a running process.
99///
100/// This used to be two hand-rolled implementations, one per host. The Unix
101/// one signalled zero and read `errno`; the Windows one opened a handle and
102/// called success "alive" -- which it is not, because a process that has
103/// exited can still be opened while any handle to it remains, and would have
104/// been reported as running.
105///
106/// `verify_pid` already asks `platform::process`, which holds a reference
107/// the kernel will not recycle and checks the exit status rather than the
108/// handle. `cleanup/verify_artifacts.rs` next door already calls it.
109fn process_is_alive(pid: u32) -> bool {
110    crate::broker::backend_lifecycle::verify_pid::process_is_alive(pid)
111}