#![allow(clippy::cast_sign_loss)]
#![allow(clippy::cast_possible_wrap)]
use std::io::{BufRead, BufReader};
use std::process::{Command, Stdio};
use std::thread;
use std::time::{Duration, Instant};
use crate::WaitHandle;
const FUSE_DURATION_SEC: u32 = 60;
const WAIT_DURATION: Duration = Duration::from_secs(1);
const TOLERANCE: Duration = Duration::from_millis(500);
fn _assert_wait_handle_send(h: WaitHandle) -> impl Send {
h
}
fn command_for_targets(unix_args: &[&str], windows_args: &[&str]) -> Command {
let args = if cfg!(unix) {
unix_args
} else if cfg!(windows) {
windows_args
} else {
unreachable!();
};
let mut command = Command::new(args[0]);
command.args(&args[1..]);
command
}
#[test]
fn timeout() {
let pid = std::process::id() as i32;
let mut pid = WaitHandle::open(pid).unwrap();
assert!(pid.wait_timeout(Duration::ZERO).unwrap().is_none());
let inst = Instant::now();
assert!(pid.wait_timeout(WAIT_DURATION).unwrap().is_none());
let elapsed = inst.elapsed();
let diff = elapsed
.checked_sub(WAIT_DURATION)
.or_else(|| WAIT_DURATION.checked_sub(elapsed))
.unwrap();
assert!(diff < TOLERANCE, "poor timeout precision? {elapsed:?}");
}
#[test]
fn invalid() {
WaitHandle::open(-1).expect_err("-1 should not be valid");
}
#[test]
fn non_existing() {
WaitHandle::open(0xDEAD).expect_err("should not exist");
}
#[cfg(any(target_os = "linux", windows))]
#[test]
fn zombie() {
use std::io::{ErrorKind, Read};
let mut child = command_for_targets(&["true"], &["cmd", "/c"])
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("failed to spawn");
let pid = child.id();
let mut stdout = child.stdout.take().unwrap();
loop {
match stdout.read(&mut [0u8]) {
Ok(0) => break,
Ok(_) => unreachable!(),
Err(e) if e.kind() == ErrorKind::Interrupted => {}
Err(e) => panic!("read failed: {e}"),
}
}
let mut pid = WaitHandle::open(pid as _).expect("should open zombie");
child.wait().unwrap();
let inst = Instant::now();
pid.wait_timeout(Duration::ZERO).unwrap().unwrap();
pid.wait_timeout(Duration::new(1, 0)).unwrap().unwrap();
pid.wait().unwrap();
let elapsed = inst.elapsed();
assert!(elapsed < TOLERANCE, "no blocking");
}
#[test]
fn child() {
let mut child = command_for_targets(&["sleep", "5"], &["timeout", "5"])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("failed to spawn");
let mut pid = WaitHandle::open(child.id() as _).unwrap();
assert!(pid.wait_timeout(Duration::ZERO).unwrap().is_none());
child.kill().unwrap();
child.wait().unwrap();
let inst = Instant::now();
pid.wait_timeout(Duration::ZERO).unwrap().unwrap();
pid.wait_timeout(Duration::new(1, 0)).unwrap().unwrap();
pid.wait().unwrap();
let elapsed = inst.elapsed();
assert!(elapsed < TOLERANCE, "no blocking");
}
#[test]
fn non_child() {
let mut child = command_for_targets(
&[
"sh",
"-c",
&format!(
"sleep {FUSE_DURATION_SEC} &\n\
echo $!"
),
],
&[
"powershell",
"-Command",
&format!(
"(Start-Process \
-PassThru \
-FilePath timeout \
-ArgumentList {FUSE_DURATION_SEC} \
-WindowStyle Hidden \
).Id"
),
],
)
.stdin(Stdio::null())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.unwrap();
let raw_pid = {
let mut buf = String::new();
BufReader::new(child.stdout.as_mut().unwrap())
.read_line(&mut buf)
.unwrap();
buf.trim().parse::<i32>().unwrap()
};
assert!(raw_pid >= 2);
assert_ne!(child.id(), raw_pid as u32);
#[cfg(unix)]
let rustix_pid = {
use rustix::io::Errno;
use rustix::process::{waitpid, Pid, WaitOptions};
let pid = Pid::from_raw(raw_pid).unwrap();
assert_eq!(
waitpid(Some(pid), WaitOptions::NOHANG).unwrap_err(),
Errno::CHILD
);
pid
};
let mut pid = WaitHandle::open(raw_pid).unwrap();
assert!(pid.wait_timeout(Duration::ZERO).unwrap().is_none());
let wait_thread = thread::spawn(move || pid.wait());
thread::sleep(WAIT_DURATION);
assert!(
!wait_thread.is_finished(),
"Returned while the process still alive: {:?}",
wait_thread.join(),
);
let inst = Instant::now();
#[cfg(unix)]
rustix::process::kill_process(rustix_pid, rustix::process::Signal::TERM).unwrap();
#[cfg(windows)]
unsafe {
use windows_sys::Win32::Foundation::CloseHandle;
use windows_sys::Win32::System::Threading::{
OpenProcess, TerminateProcess, PROCESS_TERMINATE,
};
let hprocess = OpenProcess(PROCESS_TERMINATE, 0 , raw_pid as u32);
if hprocess.is_null() {
panic!(
"failed to open process: {}",
std::io::Error::last_os_error()
);
}
let ret = TerminateProcess(hprocess, 1);
if ret == 0 {
panic!(
"failed to terminate process: {}",
std::io::Error::last_os_error()
);
}
CloseHandle(hprocess);
}
wait_thread
.join()
.expect("must not panic")
.expect("must succeed");
let elapsed = inst.elapsed();
assert!(elapsed < TOLERANCE, "Wait for too long? {elapsed:?}");
child.wait().unwrap();
}