///|
pub(all) enum ChildExit {
Exited(Int)
Signaled(Int)
} derive(Debug, Eq)
///|
pub extend ChildExit with Debug::{to_repr}
///|
pub extend ChildExit with Eq::{not_equal, equal}
///|
fn get_i32_le(bytes : Bytes, offset : Int) -> Int {
bytes[offset].to_int() |
(bytes[offset + 1].to_int() << 8) |
(bytes[offset + 2].to_int() << 16) |
(bytes[offset + 3].to_int() << 24)
}
///|
/// Reap a process through a pidfd (waitid P_PIDFD). Works for adopted
/// (non-child) processes too. ECHILD means the exit was already reaped,
/// which is reported as None rather than an error.
pub fn wait_pidfd(
fd : Int,
nohang? : Bool = false,
) -> (Int, ChildExit)? raise ProcessError {
let out = Bytes::make(12, b'\x00')
ignore(c_wait_pidfd(fd, if nohang { 1 } else { 0 }, out))
let reaped = get_i32_le(out, 0)
if reaped == 0 {
return None
}
if reaped < 0 {
let errno = get_i32_le(out, 8)
if errno == 10 {
// ECHILD: exit already reaped earlier; treat as no event.
return None
}
raise Failed(op="wait_pidfd", errno~)
}
// Linux wait status ABI: low 7 bits select the exit kind.
let status = get_i32_le(out, 4)
let kind = status & 0x7f
if kind == 0 {
Some((reaped, Exited((status >> 8) & 0xff)))
} else {
Some((reaped, Signaled(kind)))
}
}
///|
/// Reap a just-killed child until it is gone (bounded), so a failed spawn
/// attempt never leaves a zombie behind. Returns immediately when the
/// child is not reapable (already reaped, adopted, or not ours).
pub fn drain_child(pid : Int) -> Unit {
for _ in 0..<50 {
let reaped = try wait_child(pid, nohang=true) catch {
_ => return
} noraise {
r => r
}
match reaped {
Some(_) => return
None => sleep_ms(10)
}
}
}
///|
/// Wait for a child with a hard deadline, through a pidfd so the wait
/// cannot be fooled by pid reuse. When the deadline passes the whole
/// process group is SIGKILLed and the reap is retried for a short bounded
/// window, so a helper that hangs (a stuck mount, a wrapper script) can
/// never freeze the single-threaded daemon forever. Returns None when no
/// exit could be observed in time.
pub fn wait_child_timeout(pid : Int, timeout_ms : Int) -> (Int, ChildExit)? {
let pidfd = try pidfd_open(pid) catch {
_ => return None
} noraise {
fd => fd
}
let deadline = @env.now() + timeout_ms.to_uint64()
let mut result : (Int, ChildExit)? = None
while result is None {
result = wait_pidfd_nohang(pidfd)
if result is None {
if @env.now() >= deadline {
@fndash.discard(() => kill_group(pid, SIGKILL))
// Bounded post-kill reap: a process stuck in uninterruptible sleep
// is reported as a timeout rather than blocking the caller.
for _ in 0..<100 {
result = wait_pidfd_nohang(pidfd)
if result is Some(_) {
break
}
sleep_ms(20)
}
break
}
sleep_ms(20)
}
}
close(pidfd)
result
}
///|
/// Non-blocking wait on a pidfd; a failed call is reported as "nothing
/// reaped yet" so the caller's deadline stays in charge of the loop.
fn wait_pidfd_nohang(fd : Int) -> (Int, ChildExit)? {
try wait_pidfd(fd, nohang=true) catch {
_ => None
} noraise {
r => r
}
}
///|
/// Reap a child with waitpid. `pid = -1` reaps any child.
/// Returns None when `nohang` is set and no child has exited.
pub fn wait_child(
pid : Int,
nohang? : Bool = false,
) -> (Int, ChildExit)? raise ProcessError {
let out = Bytes::make(12, b'\x00')
ignore(c_waitpid(pid, if nohang { 1 } else { 0 }, out))
let reaped = get_i32_le(out, 0)
if reaped == 0 {
return None
}
if reaped < 0 {
raise Failed(op="waitpid", errno=get_i32_le(out, 8))
}
// Linux wait status ABI: low 7 bits select the exit kind.
let status = get_i32_le(out, 4)
let kind = status & 0x7f
if kind == 0 {
Some((reaped, Exited((status >> 8) & 0xff)))
} else {
Some((reaped, Signaled(kind)))
}
}