#![deny(unsafe_code)]
#[cfg(all(unix, feature = "test-support"))]
pub mod fork_exec_test;
pub mod launch_nonce;
pub mod privacy_identity;
pub mod process_identity;
#[cfg(unix)]
pub use launch_nonce::LaunchNonceHandoff;
pub use launch_nonce::{
launch_nonce, LaunchNonce, LaunchNonceError, LaunchNonceSource, LAUNCH_NONCE_ENV,
LAUNCH_NONCE_FD, LAUNCH_NONCE_FD_ENV,
};
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "macos")]
mod macos;
#[cfg(target_os = "linux")]
use linux as platform;
#[cfg(target_os = "macos")]
use macos as platform;
use std::{io, path::Path};
pub const PROCESS_IDENTITY_SUPPORTED: bool = cfg!(any(target_os = "linux", target_os = "macos"));
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct FileIdentity {
pub device: u64,
pub inode: u64,
}
pub fn file_identity(path: &Path) -> Option<FileIdentity> {
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
std::fs::metadata(path).ok().map(|metadata| FileIdentity {
device: metadata.dev(),
inode: metadata.ino(),
})
}
#[cfg(not(unix))]
{
let _ = path;
None
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Observation {
pub start_time: u64,
pub executable: Option<FileIdentity>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Signal {
Terminate,
Kill,
}
pub fn start_time(pid: u32) -> Option<u64> {
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
platform::start_time(pid)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
let _ = pid;
None
}
}
pub const RESOURCE_USAGE_SUPPORTED: bool = cfg!(any(target_os = "linux", target_os = "macos"));
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MemoryKind {
PhysFootprint,
ResidentSet,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResourceUsage {
pub memory_bytes: u64,
pub memory_kind: MemoryKind,
pub swap_bytes: Option<u64>,
pub cpu_user: std::time::Duration,
pub cpu_system: std::time::Duration,
}
pub fn resource_usage(pid: u32) -> Option<ResourceUsage> {
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
platform::resource_usage(pid)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
let _ = pid;
None
}
}
#[derive(Debug)]
pub struct Process {
pid: u32,
#[cfg(target_os = "linux")]
pidfd: Option<std::os::fd::OwnedFd>,
}
impl Process {
pub fn open(pid: u32) -> io::Result<Option<Self>> {
#[cfg(target_os = "linux")]
{
linux::open(pid).map(|opened| opened.map(|pidfd| Self { pid, pidfd }))
}
#[cfg(target_os = "macos")]
{
Ok(platform::exists(pid).then_some(Self { pid }))
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
let _ = pid;
Err(io::Error::new(
io::ErrorKind::Unsupported,
"process identity is not available on this platform",
))
}
}
pub fn pid(&self) -> u32 {
self.pid
}
pub fn signals_through_pidfd(&self) -> bool {
#[cfg(target_os = "linux")]
{
self.pidfd.is_some()
}
#[cfg(not(target_os = "linux"))]
{
false
}
}
pub fn observe(&self) -> Option<Observation> {
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
#[cfg(target_os = "linux")]
if !linux::pidfd_alive(self.pidfd.as_ref()) {
return None;
}
let start_time = platform::start_time(self.pid)?;
Some(Observation {
start_time,
executable: platform::executable_identity(self.pid),
})
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
None
}
}
pub fn signal(&self, signal: Signal) -> io::Result<bool> {
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
#[cfg(target_os = "linux")]
let result = linux::signal(self.pid, self.pidfd.as_ref(), signal);
#[cfg(target_os = "macos")]
let result = macos::signal(self.pid, signal);
match result {
Ok(()) => Ok(true),
Err(rustix::io::Errno::SRCH) => Ok(false),
Err(error) => Err(error.into()),
}
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
let _ = signal;
Err(io::Error::new(
io::ErrorKind::Unsupported,
"process signalling is not available on this platform",
))
}
}
}
#[cfg(all(test, any(target_os = "linux", target_os = "macos")))]
mod tests {
use std::{
process::{Child, Command},
time::{Duration, Instant},
};
use super::*;
fn spawn_sleep() -> Child {
Command::new("sleep")
.arg("60")
.spawn()
.expect("spawn sleep")
}
fn sleep_identity() -> FileIdentity {
let path = ["/bin/sleep", "/usr/bin/sleep"]
.into_iter()
.find(|path| Path::new(path).exists())
.expect("sleep is installed");
file_identity(Path::new(path)).expect("stat sleep")
}
fn wait_for_executable(process: &Process, expected: FileIdentity) -> Observation {
let deadline = Instant::now() + Duration::from_secs(5);
loop {
let observation = process.observe().expect("child is alive");
if observation.executable == Some(expected) || Instant::now() > deadline {
return observation;
}
std::thread::sleep(Duration::from_millis(10));
}
}
#[test]
fn own_process_is_observable_with_its_own_image() {
let process = Process::open(std::process::id())
.expect("open own process")
.expect("own process exists");
let observation = process.observe().expect("own process is alive");
let own_image = file_identity(&std::env::current_exe().unwrap()).unwrap();
assert_eq!(observation.executable, Some(own_image));
assert_eq!(start_time(std::process::id()), Some(observation.start_time));
}
#[test]
fn child_start_time_is_stable_and_differs_from_ours() {
let mut child = spawn_sleep();
let pid = child.id();
let process = Process::open(pid).unwrap().unwrap();
let observation = wait_for_executable(&process, sleep_identity());
assert_eq!(observation.executable, Some(sleep_identity()));
assert_eq!(start_time(pid), Some(observation.start_time));
child.kill().unwrap();
child.wait().unwrap();
}
#[test]
fn a_signalled_and_unreaped_child_reads_as_exited() {
let mut child = spawn_sleep();
let process = Process::open(child.id()).unwrap().unwrap();
assert!(process.signal(Signal::Terminate).unwrap());
let deadline = Instant::now() + Duration::from_secs(5);
while process.observe().is_some() {
assert!(Instant::now() < deadline, "child still observed as alive");
std::thread::sleep(Duration::from_millis(10));
}
assert_eq!(start_time(child.id()), None);
child.wait().unwrap();
}
#[test]
fn a_reaped_child_cannot_be_opened_or_observed() {
let mut child = spawn_sleep();
let pid = child.id();
child.kill().unwrap();
child.wait().unwrap();
if let Some(process) = Process::open(pid).unwrap() {
if let Some(observation) = process.observe() {
assert_ne!(observation.executable, Some(sleep_identity()));
}
}
}
#[cfg(target_os = "macos")]
#[test]
fn macos_start_time_is_microseconds_since_the_epoch() {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_micros() as u64;
let started = start_time(std::process::id()).unwrap();
assert!(started <= now, "start time {started} is after now {now}");
assert!(
now - started < 3_600 * 1_000_000,
"start time {started} is more than an hour before now {now}"
);
}
fn thread_cpu_time() -> Duration {
let now = rustix::time::clock_gettime(rustix::time::ClockId::ThreadCPUTime);
Duration::new(now.tv_sec as u64, now.tv_nsec as u32)
}
fn burn_cpu(cpu: Duration) {
let start = thread_cpu_time();
let give_up = Instant::now() + Duration::from_secs(60);
let mut value = 0u64;
while thread_cpu_time().saturating_sub(start) < cpu {
assert!(
Instant::now() < give_up,
"thread CPU clock stopped advancing"
);
for step in 0..10_000u64 {
value = std::hint::black_box(value.wrapping_mul(31).wrapping_add(step));
}
}
std::hint::black_box(value);
}
#[test]
fn own_resource_usage_is_present_and_plausible() {
let pages = vec![0xa5u8; 8 * 1024 * 1024];
std::hint::black_box(&pages);
let usage = resource_usage(std::process::id()).expect("own process is readable");
assert!(
usage.memory_bytes >= pages.len() as u64,
"memory {} bytes cannot account for {} touched private bytes",
usage.memory_bytes,
pages.len()
);
std::hint::black_box(&pages);
assert!(
usage.memory_bytes < 64 * 1024 * 1024 * 1024,
"memory {} bytes is implausibly large",
usage.memory_bytes
);
#[cfg(target_os = "macos")]
assert_eq!(usage.memory_kind, MemoryKind::PhysFootprint);
#[cfg(target_os = "linux")]
{
assert_eq!(usage.memory_kind, MemoryKind::ResidentSet);
assert!(usage.swap_bytes.is_some(), "Linux reports VmSwap");
}
}
#[test]
fn own_cpu_time_grows_by_about_the_busy_work_done() {
let pid = std::process::id();
let total = |usage: ResourceUsage| usage.cpu_user + usage.cpu_system;
let before = total(resource_usage(pid).unwrap());
let busy = Duration::from_millis(400);
burn_cpu(busy);
let after = total(resource_usage(pid).unwrap());
let grown = after.saturating_sub(before);
assert!(
grown >= busy * 9 / 10,
"cpu time grew by {grown:?} over {busy:?} of busy work"
);
}
#[test]
fn a_child_reads_its_own_usage_not_ours() {
let mut child = spawn_sleep();
let process = Process::open(child.id()).unwrap().unwrap();
wait_for_executable(&process, sleep_identity());
let ours = resource_usage(std::process::id()).unwrap();
let usage = resource_usage(child.id()).expect("live child is readable");
assert!(usage.memory_bytes > 0);
assert!(
usage.memory_bytes < ours.memory_bytes,
"a sleeping child ({} bytes) should be smaller than the test binary ({} bytes)",
usage.memory_bytes,
ours.memory_bytes
);
child.kill().unwrap();
child.wait().unwrap();
}
#[test]
fn an_exited_child_reads_as_unavailable_not_zero() {
let mut child = spawn_sleep();
let pid = child.id();
child.kill().unwrap();
let deadline = Instant::now() + Duration::from_secs(5);
while start_time(pid).is_some() {
assert!(Instant::now() < deadline, "child still observed as alive");
std::thread::sleep(Duration::from_millis(10));
}
assert_eq!(resource_usage(pid), None, "a zombie reads as unavailable");
child.wait().unwrap();
if let Some(usage) = resource_usage(pid) {
assert!(usage.memory_bytes > 0, "a reused pid is some live process");
}
}
#[test]
fn a_pid_with_no_process_reads_as_unavailable() {
assert_eq!(resource_usage(i32::MAX as u32), None);
assert_eq!(resource_usage(u32::MAX), None);
}
#[cfg(target_os = "linux")]
#[test]
fn linux_signals_go_through_a_pidfd() {
let mut child = spawn_sleep();
let process = Process::open(child.id()).unwrap().unwrap();
assert!(process.signals_through_pidfd());
child.kill().unwrap();
child.wait().unwrap();
assert!(!process.signal(Signal::Kill).unwrap());
}
}