use std::time::Duration;
use nix::sys::signal::{self, Signal};
use nix::sys::wait::{WaitPidFlag, WaitStatus, waitpid};
use nix::unistd::Pid;
use tokio::signal::unix::{SignalKind, signal as unix_signal};
use crate::exit::ExitCode;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Reaped {
Supervisor(u8),
Orphan,
Nothing,
NoChildren,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WaitOutcome {
Exited {
pid: i32,
code: i32,
},
Signaled {
pid: i32,
signal: i32,
},
StillAlive,
NoChildren,
}
#[must_use]
pub fn classify(status: WaitOutcome, supervisor: i32) -> Reaped {
match status {
WaitOutcome::Exited { pid, code } if pid == supervisor => Reaped::Supervisor(code as u8),
WaitOutcome::Exited { .. } => Reaped::Orphan,
WaitOutcome::Signaled { pid, signal } if pid == supervisor => {
Reaped::Supervisor((128 + signal) as u8)
}
WaitOutcome::Signaled { .. } => Reaped::Orphan,
WaitOutcome::StillAlive => Reaped::Nothing,
WaitOutcome::NoChildren => Reaped::NoChildren,
}
}
fn outcome(result: Result<WaitStatus, nix::errno::Errno>) -> WaitOutcome {
use nix::errno::Errno;
match result {
Ok(WaitStatus::Exited(pid, code)) => WaitOutcome::Exited {
pid: pid.as_raw(),
code,
},
Ok(WaitStatus::Signaled(pid, sig, _core_dumped)) => WaitOutcome::Signaled {
pid: pid.as_raw(),
signal: sig as i32,
},
Ok(WaitStatus::StillAlive) => WaitOutcome::StillAlive,
Err(Errno::ECHILD) => WaitOutcome::NoChildren,
Ok(other) => {
eprintln!(
"shep runtime: init's waitpid reported an unexpected status {other:?} (ignored)"
);
WaitOutcome::StillAlive
}
Err(errno) => {
eprintln!(
"shep runtime: init's waitpid reported {errno} (ignored, not fatal to PID 1)"
);
WaitOutcome::StillAlive
}
}
}
fn drain(supervisor: i32) -> Option<u8> {
loop {
let result = waitpid(Pid::from_raw(-1), Some(WaitPidFlag::WNOHANG));
match classify(outcome(result), supervisor) {
Reaped::Supervisor(status) => return Some(status),
Reaped::Orphan => continue,
Reaped::Nothing | Reaped::NoChildren => return None,
}
}
}
#[must_use]
pub const fn should_split(pid: u32, supervise: bool, forced: bool) -> bool {
!supervise && (pid == 1 || forced)
}
fn supervisor_command() -> std::io::Result<std::process::Command> {
let exe = std::env::current_exe()?;
let mut cmd = std::process::Command::new(exe);
cmd.args(std::env::args_os().skip(1)).arg("--supervise");
Ok(cmd)
}
async fn init_loop() -> i32 {
macro_rules! armed_or_fail {
($kind:expr, $name:literal) => {
match unix_signal($kind) {
Ok(stream) => stream,
Err(err) => {
eprintln!(
"shep runtime: init could not register a {} handler: {err}",
$name
);
return i32::from(ExitCode::Failure as u8);
}
}
};
}
let mut sigterm = armed_or_fail!(SignalKind::terminate(), "SIGTERM");
let mut sigint = armed_or_fail!(SignalKind::interrupt(), "SIGINT");
let mut sighup = armed_or_fail!(SignalKind::hangup(), "SIGHUP");
let mut sigquit = armed_or_fail!(SignalKind::quit(), "SIGQUIT");
let mut sigchld = armed_or_fail!(SignalKind::child(), "SIGCHLD");
let supervisor_pid = match supervisor_command().and_then(|mut cmd| cmd.spawn()) {
Ok(child) => child.id() as i32,
Err(err) => {
eprintln!("shep runtime: init could not start the supervisor: {err}");
return i32::from(ExitCode::Failure as u8);
}
};
eprintln!("shep runtime: init supervising pid {supervisor_pid}");
let mut ticker = tokio::time::interval(Duration::from_secs(1));
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
fn forward_if_live(received: Option<()>, pid: i32, sig: Signal) {
if received.is_none() {
return;
}
if let Err(err) = signal::kill(Pid::from_raw(pid), sig) {
eprintln!("shep runtime: init could not forward {sig} to pid {pid}: {err}");
}
}
loop {
tokio::select! {
received = sigterm.recv() => forward_if_live(received, supervisor_pid, Signal::SIGTERM),
received = sigint.recv() => forward_if_live(received, supervisor_pid, Signal::SIGINT),
received = sighup.recv() => forward_if_live(received, supervisor_pid, Signal::SIGHUP),
received = sigquit.recv() => forward_if_live(received, supervisor_pid, Signal::SIGQUIT),
received = sigchld.recv() => {
if received.is_some() && let Some(status) = drain(supervisor_pid) {
return i32::from(status);
}
}
_ = ticker.tick() => {
if let Some(status) = drain(supervisor_pid) {
return i32::from(status);
}
}
}
}
}
pub async fn run_init() -> std::convert::Infallible {
let status = init_loop().await;
std::process::exit(status);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_supervisor_is_told_apart_from_every_orphan() {
assert_eq!(
classify(WaitOutcome::Exited { pid: 7, code: 3 }, 7),
Reaped::Supervisor(3)
);
assert_eq!(
classify(WaitOutcome::Exited { pid: 8, code: 3 }, 7),
Reaped::Orphan
);
}
#[test]
fn a_signalled_supervisor_exits_128_plus_the_signal() {
assert_eq!(
classify(WaitOutcome::Signaled { pid: 7, signal: 9 }, 7),
Reaped::Supervisor(137)
);
}
#[test]
fn no_children_and_nothing_ready_are_told_apart() {
assert_eq!(classify(WaitOutcome::NoChildren, 7), Reaped::NoChildren);
assert_eq!(classify(WaitOutcome::StillAlive, 7), Reaped::Nothing);
}
#[test]
fn the_init_split_does_not_fire_outside_pid_one() {
assert_ne!(std::process::id(), 1, "a test harness is never PID 1");
assert!(!should_split(std::process::id(), false, false));
}
#[test]
fn supervise_disables_the_split_whatever_the_pid_and_the_switch_say() {
assert!(should_split(1, false, false));
assert!(!should_split(1, true, false));
assert!(!should_split(4242, false, false));
assert!(
should_split(4242, false, true),
"the test switch reaches the init"
);
assert!(
!should_split(4242, true, true),
"and --supervise still wins"
);
assert!(!should_split(1, true, true), "and it still wins at PID 1");
}
#[cfg(target_os = "linux")]
#[test]
fn drain_reaps_a_real_reparented_orphan() {
use std::io::Read as _;
nix::sys::prctl::set_child_subreaper(true).expect("Linux has supported this since 3.4");
let mut shell = std::process::Command::new("/bin/sh")
.args(["-c", "(sleep 0.2; exit 3) & echo $!"])
.stdout(std::process::Stdio::piped())
.spawn()
.expect("spawn the shell");
let mut printed_pid = String::new();
shell
.stdout
.take()
.expect("piped stdout")
.read_to_string(&mut printed_pid)
.expect("read the grandchild's pid off the shell's stdout");
let _ = shell.wait();
let grandchild: i32 = printed_pid
.trim()
.parse()
.expect("the shell printed a valid pid");
let bogus_supervisor = -2;
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
let mut still_present = true;
while std::time::Instant::now() < deadline && still_present {
assert_eq!(
drain(bogus_supervisor),
None,
"a fictitious supervisor pid must never be reported as reaped"
);
still_present =
nix::sys::signal::kill(nix::unistd::Pid::from_raw(grandchild), None).is_ok();
if still_present {
std::thread::sleep(std::time::Duration::from_millis(20));
}
}
assert!(
!still_present,
"the grandchild (pid {grandchild}) must have been reaped by `drain`, not left a zombie"
);
}
}