use anyhow::{Result, bail};
pub fn birth(pid: u32) -> Option<u64> {
if pid == 0 {
return None;
}
imp::birth(pid)
}
pub fn is_alive(pid: u32, born: u64) -> bool {
birth(pid) == Some(born)
}
pub fn own_birth() -> u64 {
birth(std::process::id()).unwrap_or_default()
}
pub fn own_group() -> Option<i32> {
imp::own_group()
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Ending {
Interrupt,
Terminate,
Kill,
}
pub fn detach(command: &mut std::process::Command) {
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
const CREATE_NEW_PROCESS_GROUP: u32 = 0x0000_0200;
const CREATE_NO_WINDOW: u32 = 0x0800_0000;
command.creation_flags(CREATE_NO_WINDOW | CREATE_NEW_PROCESS_GROUP);
}
#[cfg(unix)]
{
use std::os::unix::process::CommandExt;
unsafe {
command.pre_exec(|| {
if libc::setsid() == -1 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
}
}
pub fn end(pid: u32, group: Option<i32>, how: Ending) -> Result<()> {
if pid <= 1 || pid > i32::MAX as u32 {
bail!("refusing to signal pid {pid}: not a process id");
}
if let Some(group) = group
&& group <= 1
{
bail!("refusing to signal process group {group}: not a group of ours");
}
imp::end(pid, group, how)
}
#[cfg(windows)]
mod imp {
use anyhow::{Context, Result, bail};
use windows_sys::Win32::Foundation::{CloseHandle, FILETIME, STILL_ACTIVE};
use windows_sys::Win32::System::Threading::{GetExitCodeProcess, GetProcessTimes, OpenProcess, PROCESS_QUERY_LIMITED_INFORMATION};
use super::Ending;
pub fn birth(pid: u32) -> Option<u64> {
unsafe {
let handle = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, 0, pid);
if handle.is_null() {
return None;
}
let mut code = 0u32;
let running = GetExitCodeProcess(handle, &mut code) != 0 && code == STILL_ACTIVE as u32;
let zero = FILETIME {
dwLowDateTime: 0,
dwHighDateTime: 0,
};
let (mut created, mut exited, mut kernel, mut user) = (zero, zero, zero, zero);
let timed = GetProcessTimes(handle, &mut created, &mut exited, &mut kernel, &mut user) != 0;
CloseHandle(handle);
(running && timed).then(|| (u64::from(created.dwHighDateTime) << 32) | u64::from(created.dwLowDateTime))
}
}
pub fn own_group() -> Option<i32> {
None
}
pub fn end(pid: u32, _group: Option<i32>, _how: Ending) -> Result<()> {
let output = std::process::Command::new("taskkill")
.args(["/PID", &pid.to_string(), "/T", "/F"])
.output()
.context("cannot run taskkill")?;
if !output.status.success() {
bail!("taskkill failed: {}", String::from_utf8_lossy(&output.stderr).trim());
}
Ok(())
}
}
#[cfg(unix)]
mod imp {
use anyhow::Result;
use super::Ending;
#[cfg(target_os = "linux")]
pub fn birth(pid: u32) -> Option<u64> {
let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
parse_stat(&stat)
}
#[cfg(any(target_os = "linux", test))]
pub fn parse_stat(stat: &str) -> Option<u64> {
let rest = &stat[stat.rfind(')')? + 1..];
let mut fields = rest.split_whitespace();
let state = fields.next()?;
if state == "Z" || state == "X" {
return None;
}
fields.nth(18)?.parse().ok()
}
#[cfg(target_os = "macos")]
pub fn birth(pid: u32) -> Option<u64> {
unsafe {
let mut info: libc::proc_bsdinfo = std::mem::zeroed();
let size = std::mem::size_of::<libc::proc_bsdinfo>() as libc::c_int;
let written = libc::proc_pidinfo(pid as libc::c_int, libc::PROC_PIDTBSDINFO, 0, std::ptr::from_mut(&mut info).cast(), size);
if written != size || info.pbi_status == libc::SZOMB {
return None;
}
Some(info.pbi_start_tvsec * 1_000_000 + info.pbi_start_tvusec)
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
pub fn birth(pid: u32) -> Option<u64> {
(unsafe { libc::kill(pid as libc::pid_t, 0) } == 0).then_some(0)
}
pub fn own_group() -> Option<i32> {
let (group, own) = unsafe { (libc::getpgrp(), libc::getpid()) };
(group == own).then_some(group)
}
pub fn end(pid: u32, group: Option<i32>, how: Ending) -> Result<()> {
let signal = match how {
Ending::Interrupt => libc::SIGINT,
Ending::Terminate => libc::SIGTERM,
Ending::Kill => libc::SIGKILL,
};
let target = group.map_or(pid as libc::pid_t, |group| -group);
if unsafe { libc::kill(target, signal) } != 0 {
let error = std::io::Error::last_os_error();
anyhow::bail!(
"cannot signal {}: {error}",
group.map_or(format!("pid {pid}"), |group| format!("process group {group}"))
);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_live_process_is_known_by_its_birth() {
let pid = std::process::id();
let born = birth(pid).expect("this process is running");
assert_eq!(birth(pid), Some(born));
assert!(is_alive(pid, born));
assert!(!is_alive(pid, born.wrapping_add(1)));
}
#[test]
fn a_pid_nobody_has_is_not_alive() {
assert_eq!(birth(0), None);
assert_eq!(birth(i32::MAX as u32), None);
}
#[test]
fn an_exited_child_is_not_alive() {
let mut command = if cfg!(windows) {
let mut command = std::process::Command::new("cmd");
command.args(["/C", "exit 0"]);
command
} else {
std::process::Command::new("true")
};
let child = command.spawn().unwrap();
let pid = child.id();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
while birth(pid).is_some() {
assert!(std::time::Instant::now() < deadline, "pid {pid} still reads as alive after exiting");
std::thread::sleep(std::time::Duration::from_millis(20));
}
drop(child);
}
#[test]
fn end_refuses_a_number_that_is_not_a_pid() {
for pid in [0, 1, u32::MAX, i32::MAX as u32 + 1] {
let error = end(pid, None, Ending::Terminate).unwrap_err().to_string();
assert!(error.contains("not a process id"), "pid {pid}: {error}");
}
for group in [-1, 0, 1] {
let error = end(4242, Some(group), Ending::Terminate).unwrap_err().to_string();
assert!(error.contains("not a group of ours"), "group {group}: {error}");
}
}
#[cfg(unix)]
#[test]
fn the_stat_line_is_read_past_a_hostile_name() {
let fields = |state: &str| format!("4242 (a) b (c) {state} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 987654 20 21");
assert_eq!(imp::parse_stat(&fields("S")), Some(987_654));
assert_eq!(imp::parse_stat(&fields("R")), Some(987_654));
assert_eq!(imp::parse_stat(&fields("Z")), None);
assert_eq!(imp::parse_stat("garbage"), None);
}
}