use crate::engine::tmux::client_registry::{
ClientEntry, TMUX_CONTROL_ARGV_PREFIX, is_orphaned_tracked_client,
};
use crate::process_identity::{ProcessIdentity, argv_has_prefix, process_identity};
use tracing::warn;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OrphanCount {
pub total: u32,
pub tracked: u32,
pub untracked: u32,
pub scan_degraded: bool,
}
pub fn is_untracked_orphan(ident: &ProcessIdentity) -> bool {
argv_has_prefix(&ident.argv, TMUX_CONTROL_ARGV_PREFIX) && ident.ppid == 1
}
pub fn count_orphans() -> OrphanCount {
let entries = crate::engine::tmux::client_registry::global()
.map(|registry| registry.entries())
.unwrap_or_default();
let tracked: Vec<(ClientEntry, ProcessIdentity)> = entries
.into_iter()
.filter_map(|entry| process_identity(entry.pid).map(|ident| (entry, ident)))
.collect();
let (candidates, scan_degraded) = scan_tmux_control_clients();
count_orphans_from(&tracked, &candidates, scan_degraded)
}
pub fn count_orphans_from(
tracked: &[(ClientEntry, ProcessIdentity)],
candidates: &[ProcessIdentity],
scan_degraded: bool,
) -> OrphanCount {
let mut tracked_orphans = 0u32;
for (entry, ident) in tracked {
if is_orphaned_tracked_client(ident, entry) {
tracked_orphans = tracked_orphans.saturating_add(1);
}
}
let mut untracked_orphans = 0u32;
for ident in candidates {
let covered = tracked.iter().any(|(entry, _)| entry.pid == ident.pid);
if !covered && is_untracked_orphan(ident) {
untracked_orphans = untracked_orphans.saturating_add(1);
}
}
OrphanCount {
total: tracked_orphans.saturating_add(untracked_orphans),
tracked: tracked_orphans,
untracked: untracked_orphans,
scan_degraded,
}
}
#[cfg(target_os = "linux")]
fn scan_tmux_control_clients() -> (Vec<ProcessIdentity>, bool) {
let Ok(proc_dir) = std::fs::read_dir("/proc") else {
warn!("读 /proc 失败:未登记 tmux -C 孤儿扫描降级(不静默假装成功)");
return (Vec::new(), true);
};
let mut found = Vec::new();
for dirent in proc_dir.flatten() {
let Ok(pid) = dirent.file_name().to_string_lossy().parse::<u32>() else {
continue;
};
if let Some(ident) = process_identity(pid)
&& argv_has_prefix(&ident.argv, TMUX_CONTROL_ARGV_PREFIX)
{
found.push(ident);
}
}
(found, false)
}
#[cfg(not(target_os = "linux"))]
fn scan_tmux_control_clients() -> (Vec<ProcessIdentity>, bool) {
static WARN_ONCE: std::sync::Once = std::sync::Once::new();
WARN_ONCE.call_once(|| {
warn!("非 Linux 平台无 /proc,未登记 tmux -C 孤儿扫描不可用:降级为仅统计已登记条目(孤儿总数偏低)");
});
(Vec::new(), true)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::health::test_support::{spawn_fake_tmux_control, wait_argv0};
fn ident(argv: &[&str], pid: u32, ppid: u32) -> ProcessIdentity {
ProcessIdentity {
pid,
ppid,
start_key: format!("k{pid}"),
argv: argv.iter().map(|s| s.to_string()).collect(),
}
}
#[test]
fn untracked_predicate_needs_argv_prefix_and_ppid_one() {
assert!(is_untracked_orphan(&ident(&["tmux", "-C", "attach-session"], 42, 1)));
assert!(
!is_untracked_orphan(&ident(&["tmux", "-C", "attach-session"], 42, 2345)),
"盲区如实注明:subreaper 收养(ppid ≠ 1)的未登记孤儿被漏计"
);
assert!(!is_untracked_orphan(&ident(&["vim", "tmux -C.md"], 42, 1)), "结构化前缀拒子串");
assert!(
!is_untracked_orphan(&ident(&["echo", "tmux", "-C"], 42, 1)),
"argv[0] 不等不得命中"
);
assert!(!is_untracked_orphan(&ident(&["tmux", "ls"], 42, 1)));
}
#[test]
fn count_orphans_from_dedups_and_applies_predicates() {
let tracked_orphan = ClientEntry::new(10, 100, "k10".into(), "lt_a");
let live_client = ClientEntry::new(11, 500, "k11".into(), "lt_b");
let reused = ClientEntry::new(12, 100, "STALE".into(), "lt_c");
let tracked = [
(tracked_orphan, ident(&["tmux", "-C"], 10, 1)),
(live_client, ident(&["tmux", "-C"], 11, 500)),
(reused, ident(&["tmux", "-C"], 12, 1)),
];
let candidates = [
ident(&["tmux", "-C"], 10, 1),
ident(&["tmux", "-C"], 20, 1),
ident(&["tmux", "-C"], 21, 900),
];
let count = count_orphans_from(&tracked, &candidates, true);
assert_eq!(count.tracked, 1);
assert_eq!(count.untracked, 1, "登记条目覆盖的 pid 不得重复计");
assert_eq!(count.total, 2);
assert!(count.scan_degraded);
}
#[cfg(target_os = "linux")]
#[test]
fn scan_finds_fake_control_client_by_argv() {
let mut child = spawn_fake_tmux_control(60);
let pid = child.id();
wait_argv0(pid, "tmux");
let (found, degraded) = scan_tmux_control_clients();
assert!(!degraded, "Linux 不应降级");
assert!(
found
.iter()
.any(|i| i.pid == pid && argv_has_prefix(&i.argv, TMUX_CONTROL_ARGV_PREFIX)),
"扫描应按 argv 结构化前缀找到假 tmux -C 客户端"
);
let _ = child.kill();
let _ = child.wait();
}
}