Skip to main content

entracte_lib/plugins/
detect.rs

1//! Local context probes a detector plugin can ask about, through the gated
2//! host functions in [`super::runtime`] (#156, slice 5).
3//!
4//! These are the host-performed reads: the plugin never touches the OS
5//! itself — it calls a host function, and the host runs the probe here and
6//! returns only a boolean. The matching logic is pure and unit-tested; the
7//! `sysinfo` process read is cross-platform (so it's exercised on every OS,
8//! per the coverage policy) rather than a per-OS FFI shim.
9
10use std::path::Path;
11
12use sysinfo::{ProcessesToUpdate, System};
13
14use crate::proc_match::process_match_lower;
15
16/// Whether any running process's name matches `pattern`, using the same
17/// token-aware semantics as the app-pause guard ([`process_match_lower`]) —
18/// so `zoom` matches Zoom but not `zoominfo`. Refreshes the process list on
19/// each call; callers throttle. Cross-platform via `sysinfo`, so no per-OS
20/// shim.
21pub fn process_running(pattern: &str) -> bool {
22    let target = pattern.trim().to_lowercase();
23    if target.is_empty() {
24        return false;
25    }
26    let mut sys = System::new();
27    sys.refresh_processes(ProcessesToUpdate::All, false);
28    sys.processes().values().any(|p| {
29        let name = p.name().to_string_lossy().to_lowercase();
30        process_match_lower(&name, &target)
31    })
32}
33
34/// Maximum sentinel-flag file we'll read. A flag is a tiny truthy marker, so
35/// anything larger is treated as not-a-flag rather than read into memory.
36const MAX_FLAG_BYTES: u64 = 64 * 1024;
37
38/// Whether the trimmed, lowercased contents are a recognised truthy token.
39/// Pure — the testable core of the file probe.
40pub fn flag_is_truthy(contents: &str) -> bool {
41    matches!(
42        contents.trim().to_lowercase().as_str(),
43        "1" | "true" | "yes" | "on"
44    )
45}
46
47/// Whether the sentinel file at `path` exists and holds a truthy value. A
48/// missing, oversized, or unreadable file reads as `false` (no signal).
49/// Reuses [`crate::secure_io::read_capped`] for the size-bounded read.
50pub fn read_flag(path: &Path) -> bool {
51    crate::secure_io::read_capped(path, MAX_FLAG_BYTES)
52        .map(|contents| flag_is_truthy(&contents))
53        .unwrap_or(false)
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn flag_truthy_accepts_known_tokens_only() {
62        for t in ["1", "true", "TRUE", " yes ", "On\n"] {
63            assert!(flag_is_truthy(t), "{t:?} should be truthy");
64        }
65        for f in ["0", "false", "", "maybe", "2"] {
66            assert!(!flag_is_truthy(f), "{f:?} should be falsey");
67        }
68    }
69
70    #[test]
71    fn process_running_finds_the_current_process_and_misses_a_bogus_one() {
72        // The test binary's name carries "entracte" as a whole token on every
73        // platform (the cargo target is `entracte[_lib]-<hash>`, and it fits
74        // inside Linux's 15-char `comm`), so token-aware matching finds it. A
75        // multi-token bogus target uses substring fallback and matches nothing.
76        assert!(process_running("entracte"), "should find the test binary");
77        assert!(!process_running("entracte-no-such-process-zzz"));
78        assert!(!process_running("   "));
79    }
80
81    #[test]
82    fn read_flag_reads_truthy_files_and_ignores_the_rest() {
83        let dir = crate::test_support::temp_dir();
84        let yes = dir.path().join("on.flag");
85        std::fs::write(&yes, "true").unwrap();
86        assert!(read_flag(&yes));
87
88        let no = dir.path().join("off.flag");
89        std::fs::write(&no, "0").unwrap();
90        assert!(!read_flag(&no));
91
92        assert!(!read_flag(&dir.path().join("missing.flag")));
93        // A directory is not a flag file.
94        assert!(!read_flag(dir.path()));
95
96        // A non-UTF-8 file reads as no-signal rather than erroring out.
97        let binary = dir.path().join("binary.flag");
98        std::fs::write(&binary, [0xff, 0xfe, 0x00, 0x01]).unwrap();
99        assert!(!read_flag(&binary));
100    }
101
102    #[test]
103    fn read_flag_ignores_an_oversized_file() {
104        let dir = crate::test_support::temp_dir();
105        let big = dir.path().join("big.flag");
106        std::fs::write(&big, vec![b'1'; (MAX_FLAG_BYTES + 1) as usize]).unwrap();
107        assert!(!read_flag(&big));
108    }
109}