#![cfg(unix)]
use std::io::{BufRead, BufReader};
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};
fn spawn_signal_helper_with(graceful: bool) -> (std::process::Child, u32) {
let fallow_bin = env!("CARGO_BIN_EXE_fallow");
let mut builder = Command::new(fallow_bin);
builder
.env("FALLOW_TEST_SIGNAL_HELPER", "1")
.stdout(Stdio::piped())
.stderr(Stdio::piped());
if graceful {
builder.env("FALLOW_TEST_SIGNAL_HELPER_GRACEFUL", "1");
}
let mut child = builder.spawn().expect("spawn fallow signal helper");
let stdout = child.stdout.take().expect("piped stdout");
let mut reader = BufReader::new(stdout);
let mut line = String::new();
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
line.clear();
if reader.read_line(&mut line).is_ok()
&& let Ok(pid) = line.trim().parse::<u32>()
{
return (child, pid);
}
std::thread::sleep(Duration::from_millis(20));
}
let _ = child.kill();
let _ = child.wait();
panic!("signal helper did not print PID within 5s");
}
fn wait_for_pid_dead(pid: u32, timeout: Duration) -> bool {
let deadline = Instant::now() + timeout;
while Instant::now() < deadline {
let status = Command::new("kill")
.args(["-0", &pid.to_string()])
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
match status {
Ok(s) if !s.success() => return true,
_ => {}
}
std::thread::sleep(Duration::from_millis(50));
}
false
}
fn send_signal(pid: u32, signal: &str) {
let status = Command::new("kill")
.args([signal, &pid.to_string()])
.status()
.expect("kill command spawn");
assert!(status.success(), "kill {signal} {pid} failed");
}
fn spawn_signal_helper() -> (std::process::Child, u32) {
spawn_signal_helper_with(false)
}
#[test]
fn sigint_kills_registered_child_and_exits_130() {
let (mut fallow, sleep_pid) = spawn_signal_helper();
assert!(
!wait_for_pid_dead(sleep_pid, Duration::from_millis(100)),
"inner sleep PID {sleep_pid} should be alive after helper start"
);
send_signal(fallow.id(), "-INT");
let status = fallow
.wait_timeout_compat(Duration::from_secs(10))
.expect("fallow helper exit within 10s");
assert_eq!(
status.code(),
Some(130),
"SIGINT must yield exit code 128+2=130; got {:?}",
status.code(),
);
assert!(
wait_for_pid_dead(sleep_pid, Duration::from_secs(5)),
"inner sleep PID {sleep_pid} must be dead after the signal handler drains",
);
}
#[test]
fn sigterm_kills_registered_child_and_exits_143() {
let (mut fallow, sleep_pid) = spawn_signal_helper();
assert!(
!wait_for_pid_dead(sleep_pid, Duration::from_millis(100)),
"inner sleep PID {sleep_pid} should be alive after helper start"
);
send_signal(fallow.id(), "-TERM");
let status = fallow
.wait_timeout_compat(Duration::from_secs(10))
.expect("fallow helper exit within 10s");
assert_eq!(
status.code(),
Some(143),
"SIGTERM must yield exit code 128+15=143; got {:?}",
status.code(),
);
assert!(
wait_for_pid_dead(sleep_pid, Duration::from_secs(5)),
"inner sleep PID {sleep_pid} must be dead after the signal handler drains",
);
}
#[test]
fn sigint_in_graceful_mode_drains_children_but_does_not_exit() {
let (mut fallow, sleep_pid) = spawn_signal_helper_with(true);
assert!(
!wait_for_pid_dead(sleep_pid, Duration::from_millis(100)),
"inner sleep PID {sleep_pid} should be alive after helper start"
);
send_signal(fallow.id(), "-INT");
let status = fallow
.wait_timeout_compat(Duration::from_secs(10))
.expect("graceful helper exits within 10s");
assert_eq!(
status.code(),
Some(0),
"graceful mode must exit cleanly with code 0; got {:?}",
status.code(),
);
assert!(
wait_for_pid_dead(sleep_pid, Duration::from_secs(5)),
"inner sleep PID {sleep_pid} must still be drained in graceful mode (BLOCK regression)",
);
}
trait WaitTimeoutCompat {
fn wait_timeout_compat(&mut self, dur: Duration) -> Option<std::process::ExitStatus>;
}
impl WaitTimeoutCompat for std::process::Child {
fn wait_timeout_compat(&mut self, dur: Duration) -> Option<std::process::ExitStatus> {
let deadline = Instant::now() + dur;
while Instant::now() < deadline {
if let Ok(Some(status)) = self.try_wait() {
return Some(status);
}
std::thread::sleep(Duration::from_millis(50));
}
let _ = self.kill();
self.wait().ok()
}
}