yog 0.0.1

yog: a balls-oriented session manager for lernie loops (egui frontend)
Documentation
//! macOS `lsof` liveness backend (DESIGN §10): a pure `lsof -F` parser, a
//! trait-injected runner seam, and a `#[cfg(target_os = "macos")]` spawn shim.
//!
//! Linux answers both liveness questions from `/proc` ([`super::fd_probe`],
//! [`super::lock_probe`]); macOS has no `/proc`, so it parses `lsof` output. Per
//! §10 the *parser is pure and platform-independent* — compiled and covered on
//! Linux from recorded fixtures — and **only** the spawn shim is `macos`; that
//! shim is the sole region tarpaulin excludes from the Linux denominator
//! (empirically confirmed: a `cfg(target_os = "macos")` region is not compiled
//! into the instrumented binary, so it cannot be counted).
//!
//! # `lsof -F` field grammar (argv `lsof -F pan -- <path>`)
//!
//! `-F` emits one `<tag><value>` line per field (`man lsof`, "OUTPUT FOR OTHER
//! PROGRAMS"). The tags we select and consume:
//!
//! - `p` — **process ID**; begins a *process set* (always the set's first line).
//! - `f` — **file descriptor**; begins a *file set* (one open file). `pan` does
//!   not name `f`, but lsof still delimits file sets; we treat any `f` line as a
//!   fresh-file reset for robustness across GNU/BSD builds.
//! - `a` — **file access mode** (`man lsof`, field `a`): `r` read, `w` write,
//!   `u` read *and* write; space / `-` unknown. A *writer* is `a` = `w` or `u`.
//! - `n` — **file name**. Compared against the canonicalized target (the shim
//!   canonicalizes before spawning; lsof resolves its own name —
//!   canonicalize-both-sides, mirroring the procfs backends).
//!
//! Within a file set `a` precedes `n`, so the access seen since the last file
//! boundary (`f`, `p`, or the previous `n`) is that file's mode. **Confidence:**
//! `-F` is lsof's stable inter-program contract, identical in shape on Linux and
//! macOS/BSD, so the grammar is documented, not guessed; the parser is also
//! tolerant of the `f` field being present or absent.
//!
//! # Failure semantics (§10)
//!
//! `lsof` absent, erroring, or emitting non-field output ⇒ [`Probe::Unknown`]
//! (renders the Y4 "?" badge) — never a false definite. `lsof` exits 1 for both
//! "no match" and real errors, so status alone cannot disambiguate: the shim
//! maps a spawn failure and a non-empty stderr to Unknown, while empty stdout is
//! the definite "no holder" ([`Probe::Free`]).

use super::probe::{LockProbe, Probe, WriterProbe};
use std::path::Path;

/// What an `lsof -F` scan saw about the queried target: whether *any* process
/// holds it open (the lock question) and whether any holder has it open for
/// *write* (the response.json writer question).
struct Sightings {
    any_holder: bool,
    any_writer: bool,
}

/// Parse `lsof -F pan` output, deciding [`Sightings`] for `target`. Returns
/// `None` when the bytes are not `-F` field output — invalid UTF-8, or
/// non-empty content with no process (`p`) set — which the caller maps to
/// [`Probe::Unknown`]. Empty output is a definite "no holder", not an error.
fn parse(output: &[u8], target: &Path) -> Option<Sightings> {
    let text = std::str::from_utf8(output).ok()?;
    let mut saw_process = false;
    let mut writer = false; // access mode of the file set in progress
    let mut seen = Sightings {
        any_holder: false,
        any_writer: false,
    };
    for line in text.lines() {
        let mut chars = line.chars();
        let Some(tag) = chars.next() else { continue };
        let value = chars.as_str();
        match tag {
            'p' => {
                saw_process = true;
                writer = false;
            }
            'f' => writer = false,
            'a' => writer = matches!(value.chars().next(), Some('w' | 'u')),
            'n' => {
                if Path::new(value) == target {
                    seen.any_holder = true;
                    seen.any_writer |= writer;
                }
                writer = false;
            }
            _ => {}
        }
    }
    // Non-empty output that is not a process set is lsof error text, not a
    // definite negative (§10): degrade to Unknown rather than read it as Free.
    if !saw_process && !text.trim().is_empty() {
        return None;
    }
    Some(seen)
}

/// Injected `lsof` runner (the trait-injection seam, DESIGN §10 "the
/// trait-injection pattern is the template for every new effect"): yields the
/// raw stdout of `lsof -F pan -- <target>`, or `None` when lsof could not be
/// observed at all (absent / errored). Keeping the spawn behind this seam is
/// what lets the parser and probe be 100 %-covered on Linux with a recorded
/// fixture and a fake runner; only the real macOS runner below is cfg'd out.
pub(super) trait LsofRunner {
    fn run(&self, target: &Path) -> Option<Vec<u8>>;
}

/// The macOS liveness backend: answers both probe questions by parsing an
/// injected [`LsofRunner`]'s output. One struct implements both traits because
/// a single `lsof` invocation (over the relevant target) evidences both.
pub(super) struct LsofProbe<R: LsofRunner> {
    runner: R,
}

impl<R: LsofRunner> LsofProbe<R> {
    pub(super) fn new(runner: R) -> Self {
        Self { runner }
    }

    /// Run lsof over `target` and reduce the scan to a tri-state via `held`
    /// (the per-question predicate over [`Sightings`]). A runner that could not
    /// observe, or output that is not `-F` field data, is [`Probe::Unknown`]
    /// (§10 — never a false definite).
    fn observe(&self, target: &Path, held: impl FnOnce(&Sightings) -> bool) -> Probe {
        let out = self.runner.run(target);
        match out.as_deref().and_then(|o| parse(o, target)) {
            Some(seen) if held(&seen) => Probe::Held,
            Some(_) => Probe::Free,
            None => Probe::Unknown,
        }
    }
}

impl<R: LsofRunner> LockProbe for LsofProbe<R> {
    fn lock_state(&self, inbox_dir: &Path) -> Probe {
        self.observe(inbox_dir, |s| s.any_holder)
    }
}

impl<R: LsofRunner> WriterProbe for LsofProbe<R> {
    fn writer_state(&self, path: &Path) -> Probe {
        self.observe(path, |s| s.any_writer)
    }
}

/// The production `lsof` runner: spawns the real binary. macOS-only — the sole
/// code excluded from Linux coverage (§10 CI: "nothing but the lsof spawn shim
/// is cfg'd out").
#[cfg(target_os = "macos")]
pub(super) struct SystemLsof;

#[cfg(target_os = "macos")]
impl LsofRunner for SystemLsof {
    fn run(&self, target: &Path) -> Option<Vec<u8>> {
        // `-F pan`: p = PID, a = access mode, n = name (`man lsof`). `--`
        // guards a path beginning with `-`. lsof exits 1 for both "no match"
        // and error, so status is not the failure signal: a spawn failure or a
        // non-empty stderr is Unknown; empty stdout is the definite "no holder".
        let out = std::process::Command::new("lsof")
            .args(["-F", "pan", "--"])
            .arg(target)
            .output()
            .ok()?;
        if !out.status.success() && !out.stderr.is_empty() {
            return None;
        }
        Some(out.stdout)
    }
}

/// Construct the macOS liveness probe: the real `lsof` runner behind a 2 s TTL
/// cache (DESIGN §10). `super::GitTree::from_repo` selects this by `cfg` on
/// macOS; Linux uses the `/proc` probes instead.
#[cfg(target_os = "macos")]
pub(super) fn system_probe()
-> super::probe_cache::TtlCache<LsofProbe<SystemLsof>, crate::ui_state::SystemClock> {
    super::probe_cache::TtlCache::new(LsofProbe::new(SystemLsof), crate::ui_state::SystemClock)
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::cell::Cell;
    use std::path::PathBuf;

    const INBOX: &str = "/ws/inbox/20260427T140000Z-aaaa";
    const RESPONSE: &str = "/ws/steps/20260427T140000Z-aaaa/003/response.json";

    /// A fake runner replaying one recorded output (`None` = a spawn failure /
    /// absent lsof). The recorded bytes stand in for real `lsof -F pan` output.
    struct FakeLsof {
        output: Option<Vec<u8>>,
        calls: Cell<usize>,
    }

    /// `LsofProbe` over a fake replaying `out` (owned so `format!` bytes fit).
    fn probe(out: Option<Vec<u8>>) -> LsofProbe<FakeLsof> {
        LsofProbe::new(FakeLsof {
            output: out,
            calls: Cell::new(0),
        })
    }

    impl LsofRunner for FakeLsof {
        fn run(&self, _target: &Path) -> Option<Vec<u8>> {
            self.calls.set(self.calls.get() + 1);
            self.output.clone()
        }
    }

    fn target(s: &str) -> PathBuf {
        PathBuf::from(s)
    }

    #[test]
    fn writer_open_for_write_is_holder_and_writer() {
        // A file set with `au` (read+write) over the target: held and a writer.
        // Includes an `f` delimiter and an ignored `t` (type) field.
        let out = format!("p4321\nf3\ntREG\nau\nn{RESPONSE}\n");
        let seen = parse(out.as_bytes(), &target(RESPONSE)).unwrap();
        assert!(seen.any_holder && seen.any_writer);
    }

    #[test]
    fn reader_only_is_a_holder_but_not_a_writer() {
        // `ar` (read) over the target: a holder (the lock cares) but no writer.
        let out = format!("p4321\nf3\nar\nn{INBOX}\n");
        let seen = parse(out.as_bytes(), &target(INBOX)).unwrap();
        assert!(seen.any_holder && !seen.any_writer);
    }

    #[test]
    fn write_access_w_counts_as_a_writer() {
        let out = format!("p9\naw\nn{RESPONSE}\n");
        let seen = parse(out.as_bytes(), &target(RESPONSE)).unwrap();
        assert!(seen.any_writer);
    }

    #[test]
    fn a_non_matching_name_is_no_holder() {
        // A process holding some *other* file: not our target. A blank line and
        // the `f`-reset are exercised; the write access must not leak onto the
        // following unrelated name.
        let out = "p4321\nf3\naw\nn/some/other/file\n\nf4\nn/and/another\n";
        let seen = parse(out.as_bytes(), &target(RESPONSE)).unwrap();
        assert!(!seen.any_holder && !seen.any_writer);
    }

    #[test]
    fn access_does_not_leak_across_files_without_an_f_delimiter() {
        // BSD/GNU shape with no `f`: a writer on one file, then the target with
        // no `a` of its own — the reset-on-`n` keeps it from inheriting `w`.
        let out = format!("p7\naw\nn/other\nn{RESPONSE}\n");
        let seen = parse(out.as_bytes(), &target(RESPONSE)).unwrap();
        assert!(seen.any_holder && !seen.any_writer);
    }

    #[test]
    fn empty_output_is_a_definite_no_holder() {
        let seen = parse(b"", &target(RESPONSE)).unwrap();
        assert!(!seen.any_holder && !seen.any_writer);
    }

    #[test]
    fn non_field_output_is_unparseable() {
        // lsof error text on stdout (no `p` set) is Unknown, not Free.
        assert!(parse(b"lsof: WARNING: bad argument\n", &target(RESPONSE)).is_none());
    }

    #[test]
    fn invalid_utf8_is_unparseable() {
        assert!(parse(&[0xff, 0xfe, 0x00], &target(RESPONSE)).is_none());
    }

    #[test]
    fn lock_state_reports_held_free_and_unknown() {
        let held = probe(Some(format!("p1\nf3\nar\nn{INBOX}\n").into_bytes()));
        assert_eq!(held.lock_state(&target(INBOX)), Probe::Held);
        let free = probe(Some(Vec::new()));
        assert_eq!(free.lock_state(&target(INBOX)), Probe::Free);
        let absent = probe(None);
        assert_eq!(absent.lock_state(&target(INBOX)), Probe::Unknown);
    }

    #[test]
    fn writer_state_reports_held_free_and_unknown() {
        let held = probe(Some(format!("p1\nf3\naw\nn{RESPONSE}\n").into_bytes()));
        assert_eq!(held.writer_state(&target(RESPONSE)), Probe::Held);
        // A reader-only holder is Free for the writer question.
        let reader = probe(Some(format!("p1\nf3\nar\nn{RESPONSE}\n").into_bytes()));
        assert_eq!(reader.writer_state(&target(RESPONSE)), Probe::Free);
        // Unparseable stdout degrades to Unknown, never a false Free.
        let garbled = probe(Some(b"garbage\n".to_vec()));
        assert_eq!(garbled.writer_state(&target(RESPONSE)), Probe::Unknown);
        assert_eq!(garbled.runner.calls.get(), 1);
    }
}