//! Best-effort `/proc` inspection for the activity watcher (NAP-340, Linux). //! //! Decides whether a session is **blocked waiting on the user**: walk from the //! pane's process down to the deepest descendant (the agent/shell actually on //! the terminal) and check it's parked waiting on terminal input. //! //! `/proc//syscall` would say so exactly, but it's ptrace-gated and //! unreadable for our own sessions on a default kernel, so we combine three //! *unprivileged* signals instead: //! //! 1. process state `S` (interruptible sleep — where a blocking `read()` parks), //! 2. its `wchan` is not a pure timer sleep (so `sleep 30`, which also sits in //! `S` with the terminal inherited, is excluded), and //! 3. one of its stdio fds is the pane's tty. //! //! Every step degrades to `false` on a read failure, so a non-Linux host or a //! denied `/proc` read falls back to `Idle`. /// Whether the process tree under `pane_pid` looks blocked on terminal input /// from `tty` (e.g. `/dev/pts/3`) — the signal for `AwaitingInput`. pub fn awaiting_input(pane_pid: i32, tty: &str) -> bool { let leaf = deepest_descendant(pane_pid); // Interruptible sleep is where a blocking read parks; anything else // (running, uninterruptible) is busy, not waiting on us. if read_state(leaf) != Some('S') { return false; } // A pure timer sleep (`sleep`, `usleep`) also sits in 'S' with the terminal // inherited — exclude it so it reads as Idle, not AwaitingInput. if is_timer_sleep(leaf) { return false; } fd_points_at_tty(leaf, tty) } /// Follow the (last) child link down the process tree to the deepest /// descendant. Bounded so a pathological/cyclic tree can't loop. fn deepest_descendant(pid: i32) -> i32 { let mut cur = pid; for _ in 0..64 { match last_child(cur) { Some(child) if child != cur => cur = child, _ => break, } } cur } /// The most-recently-created child of `pid`, via /// `/proc//task//children` (a space-separated pid list). The last /// entry is the newest child — typically the foreground one an agent spawned. fn last_child(pid: i32) -> Option { let contents = std::fs::read_to_string(format!("/proc/{pid}/task/{pid}/children")).ok()?; contents.split_whitespace().last()?.parse().ok() } /// Read the process state char from `/proc//stat`. fn read_state(pid: i32) -> Option { parse_stat_state(&std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?) } /// Extract the state field from a `/proc//stat` line. The format is /// `pid (comm) STATE ...`; `comm` can itself contain spaces and parens, so the /// state is the first non-space char *after the final `)`*. fn parse_stat_state(stat: &str) -> Option { let close = stat.rfind(')')?; stat[close + 1..].trim_start().chars().next() } /// Whether `pid`'s `wchan` (the kernel symbol it's blocked in) is a timer /// sleep — `hrtimer_nanosleep`, `do_nanosleep`, etc. Those are quiescent but /// *not* waiting on the user. fn is_timer_sleep(pid: i32) -> bool { std::fs::read_to_string(format!("/proc/{pid}/wchan")) .map(|w| is_timer_wchan(&w)) .unwrap_or(false) } /// Classify a `wchan` symbol as a pure timer sleep. Kept pure for testing. fn is_timer_wchan(wchan: &str) -> bool { wchan.contains("nanosleep") } /// Whether any of `pid`'s stdio fds (0/1/2) is the terminal `tty`. fn fd_points_at_tty(pid: i32, tty: &str) -> bool { (0..3).any(|fd| { std::fs::read_link(format!("/proc/{pid}/fd/{fd}")) .map(|target| target.to_string_lossy() == tty) .unwrap_or(false) }) } #[cfg(test)] mod tests { use super::*; #[test] fn parse_stat_state_handles_simple_and_tricky_comm() { assert_eq!(parse_stat_state("1234 (bash) S 1 1234 ..."), Some('S')); assert_eq!(parse_stat_state("1234 (sleep) S 1 ..."), Some('S')); assert_eq!(parse_stat_state("99 (cat) R 1 ..."), Some('R')); // comm with spaces and a paren — only the final ')' delimits it. assert_eq!(parse_stat_state("42 (weird (name)) D 1 42 ..."), Some('D')); } #[test] fn parse_stat_state_rejects_garbage() { assert_eq!(parse_stat_state("not a stat line"), None); assert_eq!(parse_stat_state("123 (comm)"), None); } #[test] fn timer_wchan_excludes_sleep_but_not_a_tty_wait() { // `sleep` blocks here — excluded. assert!(is_timer_wchan("hrtimer_nanosleep")); assert!(is_timer_wchan("do_nanosleep")); // A terminal read / poll parks here — not a timer. assert!(!is_timer_wchan("wait_woken")); assert!(!is_timer_wchan("n_tty_read")); assert!(!is_timer_wchan("do_select")); assert!(!is_timer_wchan("0")); } #[test] fn awaiting_input_is_false_for_nonexistent_pid() { // A pid that can't exist → every read fails → false (fall back to Idle). assert!(!awaiting_input(i32::MAX, "/dev/pts/0")); } }