//! Unix implementation of the platform primitives. These bodies are the
//! pre-existing inline `libc` calls, relocated unchanged so the Unix code
//! paths behave exactly as before.
use std::path::Path;
/// Test whether `pid` names a live process without signalling it.
///
/// `kill(pid, 0)` returns 0 if the process exists (regardless of whether
/// we may signal it), -1/ESRCH if it's gone, -1/EPERM if it exists but we
/// lack permission — the last counts as "alive" for our purposes.
pub fn is_alive(pid: i32) -> bool {
if pid <= 0 {
return false;
}
let rc = unsafe { libc::kill(pid, 0) };
let errno = if rc == 0 {
0
} else {
std::io::Error::last_os_error().raw_os_error().unwrap_or(0)
};
alive_from_kill(rc, errno)
}
/// How to read the result of `kill(pid, 0)`.
///
/// Split out because the EPERM arm cannot be reached on demand from a test:
/// it needs a live process this account may not signal, and under root — which
/// is how the suite runs in CI's container — there is no such process, so
/// `kill` succeeds outright and the arm is never evaluated. Deleting it
/// entirely would leave the suite green. As a pure mapping it can be checked
/// directly, whatever uid the tests happen to run as.
fn alive_from_kill(rc: i32, errno: i32) -> bool {
if rc == 0 {
return true;
}
// EPERM means the process exists but belongs to somebody else. Reading it
// as "gone" would have a non-root `runner status` declare a live daemon
// crashed and start a second one on the same work dir.
errno == libc::EPERM
}
/// True when running as root (uid 0).
pub fn is_elevated() -> bool {
(unsafe { libc::getuid() }) == 0
}
/// Forcibly terminate `pid` (SIGKILL).
pub fn force_kill(pid: i32) -> std::io::Result<()> {
let rc = unsafe { libc::kill(pid, libc::SIGKILL) };
if rc == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
/// Ask a runner process to shut down gracefully (SIGTERM). The `_pid_path`
/// is unused on Unix; on Windows it derives the named stop-event.
pub fn request_graceful_stop(pid: i32, _pid_path: &Path) -> std::io::Result<()> {
let rc = unsafe { libc::kill(pid, libc::SIGTERM) };
if rc == 0 {
Ok(())
} else {
Err(std::io::Error::last_os_error())
}
}
/// Best-effort graceful terminate of a child process we spawned (executor
/// timeout / shutdown path). On Unix this sends SIGTERM; the caller still
/// escalates to `child.kill()` (SIGKILL) after a grace window.
pub fn request_child_terminate(pid: u32) {
unsafe {
libc::kill(pid as i32, libc::SIGTERM);
}
}
/// Program + leading args used to run an arbitrary shell command string.
pub fn bare_shell() -> (&'static str, Vec<&'static str>) {
("/bin/sh", vec!["-c"])
}
#[cfg(test)]
mod tests {
use super::*;
use std::process::{Command, Stdio};
/// A child that will not exit on its own, so a liveness assertion is
/// about our signalling rather than a race with its own exit.
///
/// Wrapped in a guard that kills and reaps on drop: a test that panics
/// before it gets to its own `kill` would otherwise leave a `sleep 30`
/// behind on every failing run, and these runners are long-lived.
struct Sleeper(Option<std::process::Child>);
impl Sleeper {
fn new() -> Self {
Self(Some(
Command::new("/bin/sh")
.args(["-c", "sleep 30"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("spawn a sleeper"),
))
}
fn pid(&self) -> u32 {
self.0.as_ref().expect("child still owned").id()
}
/// The signal this child was killed by, reaping it in the process.
///
/// `wait` blocks until it actually exits, which is the deterministic
/// way to observe a signal — polling `is_alive` after a `kill` is both
/// racy and wrong, since a killed-but-unreaped child is a zombie and
/// `kill(pid, 0)` still succeeds on a zombie.
fn killed_by(&mut self) -> Option<i32> {
use std::os::unix::process::ExitStatusExt;
self.0
.as_mut()
.expect("child still owned")
.wait()
.expect("reap the child")
.signal()
}
}
impl Drop for Sleeper {
fn drop(&mut self) {
if let Some(mut child) = self.0.take() {
let _ = child.kill();
let _ = child.wait();
}
}
}
/// A pid that has been spawned, exited and reaped — so it is definitely
/// not alive, without inventing a number that might belong to somebody.
fn reaped_pid() -> i32 {
let mut child = Command::new("/bin/sh")
.args(["-c", "exit 0"])
.spawn()
.unwrap();
let pid = child.id() as i32;
child.wait().unwrap();
pid
}
#[test]
fn a_nonsense_pid_is_never_alive() {
// `kill(0, …)` signals our whole process group and `kill(-1, …)`
// every process we may signal. Both would report a bogus "alive",
// and both are catastrophic to pass on to a real signal.
assert!(!is_alive(0), "pid 0 means the process group, not a process");
assert!(!is_alive(-1), "pid -1 means every process");
assert!(!is_alive(-12345));
}
#[test]
fn this_very_process_is_alive() {
assert!(is_alive(std::process::id() as i32));
}
#[test]
fn a_process_that_exists_but_is_not_ours_to_signal_counts_as_alive() {
// The EPERM arm, checked directly. Going through `is_alive(1)` instead
// proves nothing wherever the suite runs as root — CI's container does
// — because there `kill(1, 0)` simply succeeds and this arm is never
// evaluated, so deleting it would leave the suite green.
assert!(
alive_from_kill(-1, libc::EPERM),
"EPERM means the process exists and belongs to another account"
);
}
#[test]
fn a_process_that_does_not_exist_is_not_alive() {
assert!(!alive_from_kill(-1, libc::ESRCH));
}
#[test]
fn a_successful_probe_means_alive() {
assert!(alive_from_kill(0, 0));
}
#[test]
fn an_unexpected_errno_is_not_read_as_alive() {
// Only EPERM is evidence of existence; anything else is not.
assert!(!alive_from_kill(-1, libc::EINVAL));
assert!(!alive_from_kill(-1, 0));
}
#[test]
fn pid_one_is_alive_whichever_arm_answers() {
// End-to-end sanity over the real syscall: as root this passes via
// rc == 0, unprivileged via EPERM. Both are correct; the arms
// themselves are pinned above.
assert!(is_alive(1));
}
#[test]
fn a_reaped_child_is_not_alive() {
let pid = reaped_pid();
assert!(
!is_alive(pid),
"reaped pid {pid} still reads as alive; stale PID files would never be cleaned up"
);
}
#[test]
fn a_killed_but_unreaped_child_still_reads_as_alive() {
// `kill(pid, 0)` succeeds on a zombie, so `is_alive` answers
// "does this pid exist", not "is this process running". Anything
// deciding whether a runner is up must reap or check elsewhere —
// this is a property to know about, not a bug to route around.
let mut child = Sleeper::new();
let pid = child.pid() as i32;
force_kill(pid).unwrap();
std::thread::sleep(std::time::Duration::from_millis(200));
assert!(
is_alive(pid),
"expected the zombie at {pid} to still satisfy kill(pid, 0)"
);
assert_eq!(child.killed_by(), Some(libc::SIGKILL));
assert!(!is_alive(pid), "reaping must retire the pid");
}
#[test]
fn force_kill_terminates_a_running_process() {
let mut child = Sleeper::new();
let pid = child.pid() as i32;
assert!(is_alive(pid), "the sleeper should have started");
force_kill(pid).expect("SIGKILL to our own child must succeed");
assert_eq!(
child.killed_by(),
Some(libc::SIGKILL),
"the sleeper did not die of SIGKILL"
);
}
#[test]
fn force_kill_of_an_absent_process_is_an_error_not_a_silent_success() {
// `runner stop` surfaces this; swallowing it would report having
// killed a runner that was never running.
let pid = reaped_pid();
assert!(
force_kill(pid).is_err(),
"expected an error for reaped pid {pid}"
);
}
#[test]
fn request_graceful_stop_sends_a_signal_the_process_actually_dies_from() {
// SIGTERM's default action is terminate, so a child that installs no
// handler must go away — otherwise `runner stop` hangs forever.
let mut child = Sleeper::new();
let pid = child.pid() as i32;
request_graceful_stop(pid, std::path::Path::new("/unused/on/unix.pid"))
.expect("SIGTERM to our own child must succeed");
assert_eq!(
child.killed_by(),
Some(libc::SIGTERM),
"runner stop must send SIGTERM, not something the child ignores"
);
}
#[test]
fn request_graceful_stop_reports_a_process_that_is_already_gone() {
let pid = reaped_pid();
assert!(request_graceful_stop(pid, std::path::Path::new("/x")).is_err());
}
#[test]
fn request_child_terminate_stops_a_child_we_spawned() {
let mut child = Sleeper::new();
request_child_terminate(child.pid());
assert_eq!(
child.killed_by(),
Some(libc::SIGTERM),
"the executor's timeout path never terminated the child"
);
}
#[test]
fn request_child_terminate_of_an_absent_process_does_not_panic() {
// Best-effort by contract — the caller escalates to SIGKILL.
request_child_terminate(reaped_pid() as u32);
}
#[test]
fn is_elevated_agrees_with_the_id_command() {
// Comparing against `libc::getuid` would only restate the
// implementation; `id -u` is an independent oracle.
let out = Command::new("id").arg("-u").output().expect("run id -u");
let uid: u32 = String::from_utf8_lossy(&out.stdout).trim().parse().unwrap();
assert_eq!(
is_elevated(),
uid == 0,
"is_elevated disagrees with `id -u` ({uid})"
);
}
#[test]
fn bare_shell_names_a_shell_and_the_flag_that_takes_a_command_string() {
let (program, args) = bare_shell();
assert_eq!(program, "/bin/sh");
assert_eq!(args, vec!["-c"]);
}
#[test]
fn bare_shell_actually_runs_a_command_string() {
// The tuple is only correct if it composes into a working
// invocation; asserting the strings alone would not catch a flag
// that stopped meaning "read the next argument as a command".
let (program, args) = bare_shell();
let out = Command::new(program)
.args(&args)
.arg("echo composed && exit 3")
.output()
.expect("run through the bare shell");
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), "composed");
assert_eq!(
out.status.code(),
Some(3),
"the shell's exit status must reach the caller"
);
}
}