driver-manager: reaper captures waitpid status + watchdog exit on panic (N18 Q2+Sigterm reset + Bug5)

Two changes in one file:

- Q2 mechanical: change the spawn_reaper_thread closure
  signature from `Fn(u32)` to `Fn(u32, i32)`; capture
  `waitpid` status via `&mut status`; pass both `res` and
  `status` to the reaper callback. The existing
  `reaper_thread_fires_on_flag` test's closure is updated to
  `|pid: u32, _status: i32|`.

- Bug5: `run_watchdog` no longer silently `return;`s on a
  reaper thread panic. A `TEST_NO_EXIT` cfg-conditional const
  gates the production path (`std::process::exit(1)` so init
  respawns driver-manager) vs the test path (log-and-return so
  the existing watchdog tests are not affected).

The remaining pieces of Q2 (status routing in the reaper
callback) live in the main.rs Q1+Q2+S3 commit.
This commit is contained in:
2026-07-27 21:57:30 +09:00
parent 509552725f
commit a30db01cba
@@ -21,6 +21,11 @@ use std::time::Duration;
static REAP_FLAG: AtomicBool = AtomicBool::new(false);
#[cfg(test)]
const TEST_NO_EXIT: bool = true;
#[cfg(not(test))]
const TEST_NO_EXIT: bool = false;
extern "C" fn sigchld_handler(_sig: i32) {
REAP_FLAG.store(true, Ordering::SeqCst);
}
@@ -38,7 +43,7 @@ pub fn set_reap_flag() {
/// `spawned` map).
pub fn spawn_reaper_thread<F>(on_reap: F) -> thread::JoinHandle<()>
where
F: Fn(u32) + Send + 'static,
F: Fn(u32, i32) + Send + 'static,
{
unsafe {
libc::signal(libc::SIGCHLD, sigchld_handler as *const () as libc::sighandler_t);
@@ -93,6 +98,12 @@ fn run_watchdog(handle: thread::JoinHandle<()>) {
);
}
}
if !TEST_NO_EXIT {
log::error!("driver-manager-sigchld: reaper unrecoverable; exiting driver-manager for init respawn");
std::process::exit(1);
} else {
log::warn!("driver-manager-sigchld: reaper exited (test mode, no process exit)");
}
return;
}
}
@@ -118,7 +129,7 @@ fn panic_payload_to_string(payload: &Box<dyn std::any::Any + Send>) -> String {
}
}
fn run<F: Fn(u32)>(on_reap: F) {
fn run<F: Fn(u32, i32)>(on_reap: F) {
log::info!("sigchld-reaper: worker started");
loop {
std::thread::sleep(Duration::from_millis(100));
@@ -130,12 +141,13 @@ fn run<F: Fn(u32)>(on_reap: F) {
// for a child so the call will not return success for a
// non-zombie pid. We loop until ECHILD.
loop {
let res = unsafe { libc::waitpid(-1, std::ptr::null_mut(), libc::WNOHANG) };
let mut status: i32 = 0;
let res = unsafe { libc::waitpid(-1, &mut status, libc::WNOHANG) };
if res <= 0 {
break;
}
log::info!("sigchld-reaper: reaped pid {}", res);
on_reap(res as u32);
log::info!("sigchld-reaper: reaped pid {} status 0x{:x}", res, status as u32);
on_reap(res as u32, status);
}
}
}
@@ -158,7 +170,7 @@ mod tests {
fn reaper_thread_fires_on_flag() {
let seen: std::sync::Arc<AtomicU32> = std::sync::Arc::new(AtomicU32::new(0));
let s = seen.clone();
let handle = spawn_reaper_thread(move |pid| {
let handle = spawn_reaper_thread(move |pid: u32, _status: i32| {
// The worker reads the flag, calls waitpid, and invokes
// this on every reaped pid. In the test environment we
// cannot have an actual child to reap, so the loop will