pub fn agent_running(pid: u32) -> bool {
let Ok(signed) = libc::pid_t::try_from(pid) else {
return false;
};
if signed <= 0 || unsafe { libc::kill(signed, 0) } != 0 {
return false;
}
!is_zombie(pid)
}
fn is_zombie(pid: u32) -> bool {
let Ok(status) = std::fs::read_to_string(format!("/proc/{pid}/status")) else {
return false;
};
status
.lines()
.find(|line| line.starts_with("State:"))
.and_then(|line| line.split_whitespace().nth(1))
.is_some_and(|state| state == "Z")
}
pub fn terminate(pid: u32) -> bool {
let Ok(pid) = libc::pid_t::try_from(pid) else {
return false;
};
pid > 0 && unsafe { libc::kill(pid, libc::SIGTERM) == 0 }
}
pub fn process_start_time(pid: u32) -> Option<u64> {
let stat = std::fs::read_to_string(format!("/proc/{pid}/stat")).ok()?;
let rest = &stat[stat.rfind(')')? + 1..];
rest.split_whitespace().nth(19)?.parse::<u64>().ok()
}
pub fn is_same_process(pid: u32, expected_start: u64) -> bool {
process_start_time(pid) == Some(expected_start)
}
pub fn looks_like_devflow_process(pid: u32) -> bool {
let Ok(cmdline) = std::fs::read(format!("/proc/{pid}/cmdline")) else {
return false;
};
cmdline
.split(|&byte| byte == 0)
.filter(|arg| !arg.is_empty())
.any(|arg| {
let arg = String::from_utf8_lossy(arg);
std::path::Path::new(arg.as_ref())
.file_name()
.and_then(|name| name.to_str())
.is_some_and(|name| name.starts_with("devflow"))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn agent_running_detects_self() {
assert!(agent_running(std::process::id()));
}
#[test]
fn agent_running_is_false_for_an_unreaped_zombie() {
let mut child = std::process::Command::new("true")
.spawn()
.expect("spawn true");
let pid = child.id();
let mut became_zombie = false;
for _ in 0..200 {
if super::is_zombie(pid) {
became_zombie = true;
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
assert!(became_zombie, "child never became an unreaped zombie");
assert_eq!(
unsafe { libc::kill(pid as libc::pid_t, 0) },
0,
"kill(pid, 0) is expected to still succeed on a zombie — if this \
fails the test is no longer exercising the case it was written for"
);
assert!(
!agent_running(pid),
"a zombie has exited and must not be reported as running"
);
let _ = child.wait();
}
#[test]
fn agent_running_false_for_dead_pid() {
assert!(!agent_running(0x7FFF_FFFE));
}
#[test]
fn agent_running_rejects_corrupt_pid_values() {
assert!(!agent_running(0));
assert!(!agent_running(u32::MAX));
assert!(!agent_running(i32::MAX as u32 + 1));
}
#[test]
fn terminate_rejects_pid_zero() {
assert!(!terminate(0));
}
#[test]
fn terminate_rejects_pid_above_i32_max() {
assert!(!terminate(u32::MAX));
assert!(!terminate(i32::MAX as u32 + 1));
}
#[test]
fn terminate_signals_a_live_child_and_it_exits() {
let mut child = std::process::Command::new("sleep")
.arg("30")
.spawn()
.expect("spawn sleep");
let pid = child.id();
assert!(terminate(pid), "terminate must report the signal delivered");
let status = child.wait().expect("wait on the terminated child");
assert!(
!status.success(),
"a SIGTERM'd child must not report a successful exit, got {status:?}"
);
}
#[test]
fn looks_like_devflow_process_is_true_for_the_current_process() {
assert!(looks_like_devflow_process(std::process::id()));
}
fn debug_cmdline(pid: u32) -> String {
match std::fs::read(format!("/proc/{pid}/cmdline")) {
Ok(raw) if raw.iter().all(|&byte| byte == 0) => "<empty>".to_string(),
Ok(raw) => raw
.split(|&byte| byte == 0)
.filter(|arg| !arg.is_empty())
.map(|arg| String::from_utf8_lossy(arg).into_owned())
.collect::<Vec<_>>()
.join(" | "),
Err(err) => format!("<unreadable: {err}>"),
}
}
#[test]
fn looks_like_devflow_process_is_false_for_a_non_devflow_process() {
let mut child = std::process::Command::new("sleep")
.arg("5")
.spawn()
.expect("spawn sleep");
let pid = child.id();
let cmdline_before = debug_cmdline(pid);
let verdict = looks_like_devflow_process(pid);
let cmdline_after = debug_cmdline(pid);
let exe = std::fs::read_link(format!("/proc/{pid}/exe"))
.map(|path| path.display().to_string())
.unwrap_or_else(|err| format!("<unreadable: {err}>"));
let self_pid = std::process::id();
let self_cmdline = debug_cmdline(self_pid);
let _ = child.kill();
let _ = child.wait();
assert!(
!verdict,
"looks_like_devflow_process({pid}) returned true for a spawned `sleep`.\n\
\x20 child cmdline before: {cmdline_before}\n\
\x20 child cmdline after: {cmdline_after}\n\
\x20 child /proc/{pid}/exe: {exe}\n\
\x20 test process: pid {self_pid} cmdline {self_cmdline}\n\
If both child cmdlines name a devflow binary, the pid is not the \
`sleep` we spawned (recycled/misattributed pid). If they differ from \
each other, the cmdline changed under the predicate. If they name \
`sleep`, the predicate's matching logic is at fault."
);
}
#[test]
fn looks_like_devflow_process_is_false_when_proc_cannot_be_read() {
assert!(!looks_like_devflow_process(0x7FFF_FFFE));
}
}