mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-09 03:20:56 +00:00
fix(proc): treat zombie-only process groups as dead on Linux
Linux's kill(-pgid, 0) succeeds even when every group member is a zombie waiting to be reaped; macOS returns ESRCH in the same situation. Callers polling on process_group_alive (fabro-server's SIGTERM grace loop, plus the zombie-only regression test in fabro-proc) therefore saw divergent behavior: CI on Linux had been failing for days on the asserting test. After the cheap kill(2) probe, walk /proc and confirm at least one non-zombie process still reports the given pgid. Non-Linux unix targets keep the fast path. Falls back to "alive" on /proc read failure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3d831ef273
commit
e9cd2bd5b8
1 changed files with 49 additions and 2 deletions
|
|
@ -83,11 +83,24 @@ fn unix_process_state(pid: u32) -> Option<char> {
|
|||
pub fn process_group_alive(pgid: u32) -> bool {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let Ok(pgid) = i32::try_from(pgid) else {
|
||||
let Ok(pgid_i32) = i32::try_from(pgid) else {
|
||||
return false;
|
||||
};
|
||||
// SAFETY: kill(-pgid, 0) is a read-only probe for the process group.
|
||||
unsafe { libc::kill(-pgid, 0) == 0 }
|
||||
if unsafe { libc::kill(-pgid_i32, 0) } != 0 {
|
||||
return false;
|
||||
}
|
||||
// Linux's kill(-pgid, 0) succeeds even when every member is a zombie
|
||||
// waiting to be reaped, unlike macOS. Disambiguate so callers polling
|
||||
// on group liveness don't burn their grace period on a dead group.
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
linux_group_has_non_zombie(pgid)
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
true
|
||||
}
|
||||
}
|
||||
#[cfg(not(unix))]
|
||||
{
|
||||
|
|
@ -96,6 +109,40 @@ pub fn process_group_alive(pgid: u32) -> bool {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn linux_group_has_non_zombie(pgid: u32) -> bool {
|
||||
let Ok(entries) = std::fs::read_dir("/proc") else {
|
||||
return true;
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let name = entry.file_name();
|
||||
let Some(name) = name.to_str() else {
|
||||
continue;
|
||||
};
|
||||
if !name.bytes().all(|b| b.is_ascii_digit()) {
|
||||
continue;
|
||||
}
|
||||
let Ok(stat) = std::fs::read_to_string(entry.path().join("stat")) else {
|
||||
continue;
|
||||
};
|
||||
// Format: "pid (comm) state ppid pgrp ..."; comm may contain ')' so
|
||||
// anchor on the final ')' before parsing subsequent fields.
|
||||
let Some((_, tail)) = stat.rsplit_once(')') else {
|
||||
continue;
|
||||
};
|
||||
let mut fields = tail.split_whitespace();
|
||||
let state = fields.next();
|
||||
let _ppid = fields.next();
|
||||
let Some(pgrp) = fields.next().and_then(|s| s.parse::<u32>().ok()) else {
|
||||
continue;
|
||||
};
|
||||
if pgrp == pgid && state != Some("Z") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Send SIGTERM to a single process.
|
||||
#[cfg(unix)]
|
||||
pub fn sigterm(pid: u32) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue