use std::io;
use std::process::{Child, ExitStatus};
use std::time::Instant;
use super::ProcessState;
use crate::ProcessId;
#[cfg(any(target_os = "linux", target_os = "android"))]
mod pidfd;
#[cfg(any(target_os = "linux", target_os = "android"))]
use self::pidfd as sys;
#[cfg(any(
target_vendor = "apple",
target_os = "freebsd",
target_os = "dragonfly",
target_os = "openbsd",
target_os = "netbsd"
))]
mod kqueue;
#[cfg(any(
target_vendor = "apple",
target_os = "freebsd",
target_os = "dragonfly",
target_os = "openbsd",
target_os = "netbsd"
))]
use self::kqueue as sys;
#[cfg(all(
unix,
not(any(
target_os = "linux",
target_os = "android",
target_vendor = "apple",
target_os = "freebsd",
target_os = "dragonfly",
target_os = "openbsd",
target_os = "netbsd"
))
))]
mod unsupported;
#[cfg(all(
unix,
not(any(
target_os = "linux",
target_os = "android",
target_vendor = "apple",
target_os = "freebsd",
target_os = "dragonfly",
target_os = "openbsd",
target_os = "netbsd"
))
))]
use self::unsupported as sys;
#[cfg(windows)]
mod windows;
#[cfg(windows)]
use self::windows as sys;
#[cfg(windows)]
use self::windows::unknown_state as fallback_state;
#[cfg(unix)]
use super::unix::signal_state as fallback_state;
enum Opened {
Watching(sys::Event),
Gone,
#[cfg(unix)]
Unsupported,
}
#[derive(Debug)]
enum Source {
Event(sys::Event),
Exited,
#[cfg(unix)]
Pid,
}
#[derive(Debug)]
pub struct ProcessHandle {
pid: ProcessId,
source: Source,
}
impl ProcessHandle {
pub fn open(pid: ProcessId) -> io::Result<Self> {
let source = match sys::open(pid.get())? {
Opened::Watching(event) => Source::Event(event),
Opened::Gone => Source::Exited,
#[cfg(unix)]
Opened::Unsupported => Source::Pid,
};
Ok(Self { pid, source })
}
pub fn for_child(child: &Child) -> io::Result<Self> {
Self::open(ProcessId::of(child))
}
pub fn pid(&self) -> ProcessId {
self.pid
}
pub fn is_pid_only(&self) -> bool {
#[cfg(unix)]
{
matches!(self.source, Source::Pid)
}
#[cfg(windows)]
{
false
}
}
pub fn has_exited(&mut self) -> io::Result<bool> {
self.wait_for(Some(Instant::now()))
}
pub fn wait(&mut self) -> io::Result<()> {
self.wait_for(None).map(drop)
}
pub fn wait_until(&mut self, deadline: Instant) -> io::Result<bool> {
self.wait_for(Some(deadline))
}
fn wait_for(&mut self, deadline: Option<Instant>) -> io::Result<bool> {
let exited = match &mut self.source {
Source::Event(event) => event.wait(deadline)?,
Source::Exited => true,
#[cfg(unix)]
Source::Pid => pid_wait(self.pid.get(), deadline)?,
};
if exited {
self.source = Source::Exited;
}
Ok(exited)
}
#[cfg(feature = "async")]
pub async fn exited(&mut self) -> io::Result<()> {
match &mut self.source {
Source::Event(event) => event.exited().await?,
Source::Exited => {}
#[cfg(unix)]
Source::Pid => {
let mut pause = crate::backoff::FIRST;
while !pid_exited(self.pid.get())? {
let nap = crate::backoff::next_pause(&mut pause, None).expect("no deadline");
#[expect(
clippy::disallowed_methods,
reason = "Only the PID fallback polls; supported platforms await the OS exit event."
)]
tokio::time::sleep(nap).await;
}
}
}
self.source = Source::Exited;
Ok(())
}
#[cfg(feature = "async")]
pub async fn exited_until(&mut self, deadline: tokio::time::Instant) -> io::Result<bool> {
match tokio::time::timeout_at(deadline, self.exited()).await {
Ok(result) => result.map(|()| true),
Err(_) => Ok(false),
}
}
}
pub fn process_state(pid: ProcessId) -> ProcessState {
match ProcessHandle::open(pid).and_then(|mut handle| handle.has_exited()) {
Ok(true) => ProcessState::Exited,
Ok(false) => ProcessState::Alive,
Err(_) => fallback_state(pid.get()),
}
}
pub fn process_has_exited(pid: ProcessId) -> bool {
process_state(pid) == ProcessState::Exited
}
#[cfg_attr(
not(any(target_os = "linux", target_os = "android", windows)),
allow(dead_code)
)]
fn millis_until(deadline: Instant, now: Instant) -> u128 {
deadline
.saturating_duration_since(now)
.as_nanos()
.div_ceil(1_000_000)
}
#[cfg(unix)]
fn pid_exited(pid: u32) -> io::Result<bool> {
match fallback_state(pid) {
ProcessState::Exited => Ok(true),
ProcessState::Alive => Ok(false),
ProcessState::Unknown => Err(io::Error::other(
"the OS did not say whether the process exited",
)),
}
}
#[cfg(unix)]
fn pid_wait(pid: u32, deadline: Option<Instant>) -> io::Result<bool> {
crate::backoff::poll(|| pid_exited(pid), deadline)
}
pub(crate) fn wait_child(
mut try_wait: impl FnMut() -> io::Result<Option<ExitStatus>>,
exit: Option<&mut ProcessHandle>,
deadline: Instant,
) -> io::Result<bool> {
match exit {
Some(exit) if !exit.is_pid_only() => exit.wait_until(deadline),
_ => crate::backoff::poll(|| Ok(try_wait()?.is_some()), Some(deadline)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Read;
use std::process::{Command, Stdio};
use std::time::Duration;
#[test]
#[ignore = "helper process for the exit-event tests"]
fn blocked_child_helper() {
if std::env::var_os("KDAEMON_BLOCKED_CHILD").is_none() {
return;
}
let _ = std::io::stdin().read_to_end(&mut Vec::new());
std::process::exit(3);
}
fn blocked_child() -> Child {
Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"local::process::tests::blocked_child_helper",
"--ignored",
"--nocapture",
"--test-threads=1",
])
.env("KDAEMON_BLOCKED_CHILD", "1")
.stdin(Stdio::piped())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.unwrap()
}
fn release(child: &mut Child) {
drop(child.stdin.take());
}
#[test]
fn the_exit_event_fires_only_once_the_process_exits() {
let mut child = blocked_child();
let mut exit = ProcessHandle::for_child(&child).unwrap();
assert_eq!(exit.pid(), ProcessId::of(&child));
assert!(!exit.is_pid_only(), "no exit event on this system");
assert!(!exit.has_exited().unwrap());
let started = Instant::now();
assert!(
!exit
.wait_until(started + Duration::from_millis(100))
.unwrap()
);
assert!(started.elapsed() >= Duration::from_millis(100));
assert!(child.try_wait().unwrap().is_none());
release(&mut child);
assert!(
exit.wait_until(Instant::now() + Duration::from_secs(30))
.unwrap()
);
assert!(exit.has_exited().unwrap());
assert_eq!(child.wait().unwrap().code(), Some(3));
assert!(exit.has_exited().unwrap());
exit.wait().unwrap();
}
#[test]
fn a_blocking_wait_returns_when_the_process_exits_and_not_before() {
let mut child = blocked_child();
let mut exit = ProcessHandle::for_child(&child).unwrap();
let (done, waited) = std::sync::mpsc::channel();
let waiter = std::thread::spawn(move || {
let result = exit.wait();
let _ = done.send(());
result
});
assert!(
waited.recv_timeout(Duration::from_millis(200)).is_err(),
"the wait returned while the process was still running"
);
release(&mut child);
waiter.join().unwrap().unwrap();
assert_eq!(child.wait().unwrap().code(), Some(3));
}
#[test]
fn an_exited_child_not_yet_waited_for_reads_as_exited() {
let mut child = blocked_child();
let mut first = ProcessHandle::for_child(&child).unwrap();
release(&mut child);
first.wait().unwrap();
let mut late = ProcessHandle::open(ProcessId::of(&child)).unwrap();
assert!(late.has_exited().unwrap());
assert_eq!(process_state(ProcessId::of(&child)), ProcessState::Exited);
assert!(process_has_exited(ProcessId::of(&child)));
assert_eq!(child.wait().unwrap().code(), Some(3));
}
#[test]
fn a_process_that_is_gone_reads_as_exited_at_once() {
let mut child = blocked_child();
release(&mut child);
child.wait().unwrap();
let mut exit = ProcessHandle::open(ProcessId::of(&child)).unwrap();
assert!(exit.has_exited().unwrap());
exit.wait().unwrap();
assert_eq!(process_state(ProcessId::of(&child)), ProcessState::Exited);
}
#[test]
fn the_current_process_is_alive() {
let mut own = ProcessHandle::open(ProcessId::current()).unwrap();
assert!(!own.has_exited().unwrap());
assert_eq!(process_state(ProcessId::current()), ProcessState::Alive);
assert!(!process_has_exited(ProcessId::current()));
}
#[test]
fn deadlines_round_up_to_whole_milliseconds() {
let now = Instant::now();
assert_eq!(millis_until(now, now), 0);
assert_eq!(millis_until(now, now + Duration::from_secs(1)), 0);
assert_eq!(millis_until(now + Duration::from_nanos(1), now), 1);
assert_eq!(millis_until(now + Duration::from_millis(1), now), 1);
assert_eq!(
millis_until(
now + Duration::from_millis(1) + Duration::from_nanos(1),
now
),
2
);
assert_eq!(millis_until(now + Duration::from_secs(5), now), 5000);
}
#[cfg(unix)]
fn pid_only(child: &Child) -> ProcessHandle {
ProcessHandle {
pid: ProcessId::of(child),
source: Source::Pid,
}
}
#[cfg(unix)]
#[test]
fn the_pid_fallback_waits_for_the_process_to_go() {
let mut child = blocked_child();
let mut exit = pid_only(&child);
assert!(exit.is_pid_only());
assert!(!exit.has_exited().unwrap());
let started = Instant::now();
assert!(
!exit
.wait_until(started + Duration::from_millis(100))
.unwrap()
);
assert!(started.elapsed() >= Duration::from_millis(100));
release(&mut child);
let reaper = std::thread::spawn(move || child.wait().unwrap());
assert!(
exit.wait_until(Instant::now() + Duration::from_secs(30))
.unwrap()
);
assert!(exit.has_exited().unwrap());
assert_eq!(reaper.join().unwrap().code(), Some(3));
}
#[cfg(target_os = "linux")]
fn listed(pid: u32) -> bool {
std::path::Path::new(&format!("/proc/{pid}")).exists()
}
#[test]
fn an_owned_child_is_waited_for_on_its_exit_event_and_left_unreaped() {
let mut child = blocked_child();
let mut exit = ProcessHandle::for_child(&child).unwrap();
let deadline = Instant::now() + Duration::from_millis(100);
assert!(!wait_child(|| child.try_wait(), Some(&mut exit), deadline).unwrap());
assert!(Instant::now() >= deadline);
release(&mut child);
let deadline = Instant::now() + Duration::from_secs(30);
assert!(wait_child(|| child.try_wait(), Some(&mut exit), deadline).unwrap());
#[cfg(target_os = "linux")]
assert!(listed(child.id()));
assert_eq!(child.wait().unwrap().code(), Some(3));
}
#[cfg(unix)]
#[test]
fn an_owned_child_whose_handle_follows_only_its_pid_is_asked_directly() {
let mut child = blocked_child();
let mut exit = pid_only(&child);
release(&mut child);
let started = Instant::now();
let deadline = started + Duration::from_secs(30);
assert!(wait_child(|| child.try_wait(), Some(&mut exit), deadline).unwrap());
assert!(started.elapsed() < Duration::from_secs(20));
#[cfg(target_os = "linux")]
assert!(!listed(child.id()));
assert_eq!(child.wait().unwrap().code(), Some(3));
}
#[test]
fn an_owned_child_without_a_handle_is_asked_directly() {
let mut child = blocked_child();
let deadline = Instant::now() + Duration::from_millis(100);
assert!(!wait_child(|| child.try_wait(), None, deadline).unwrap());
assert!(Instant::now() >= deadline);
release(&mut child);
let deadline = Instant::now() + Duration::from_secs(30);
assert!(wait_child(|| child.try_wait(), None, deadline).unwrap());
assert_eq!(child.wait().unwrap().code(), Some(3));
}
#[cfg(feature = "async")]
#[tokio::test]
async fn the_async_exit_event_fires_only_once_the_process_exits() {
let mut child = blocked_child();
let mut exit = ProcessHandle::for_child(&child).unwrap();
assert!(
tokio::time::timeout(Duration::from_millis(100), exit.exited())
.await
.is_err(),
"the exit future completed while the process was still running"
);
let deadline = tokio::time::Instant::now() + Duration::from_millis(50);
assert!(!exit.exited_until(deadline).await.unwrap());
release(&mut child);
let deadline = tokio::time::Instant::now() + Duration::from_secs(30);
assert!(exit.exited_until(deadline).await.unwrap());
exit.exited().await.unwrap();
assert!(exit.has_exited().unwrap());
assert_eq!(child.wait().unwrap().code(), Some(3));
}
#[cfg(all(unix, feature = "async"))]
#[tokio::test]
async fn the_async_pid_fallback_waits_for_the_process_to_go() {
let mut child = blocked_child();
let mut exit = pid_only(&child);
assert!(
tokio::time::timeout(Duration::from_millis(100), exit.exited())
.await
.is_err()
);
release(&mut child);
let reaper = std::thread::spawn(move || child.wait().unwrap());
let deadline = tokio::time::Instant::now() + Duration::from_secs(30);
assert!(exit.exited_until(deadline).await.unwrap());
assert_eq!(reaper.join().unwrap().code(), Some(3));
}
}