use crate::js_text::trim_javascript;
use std::sync::OnceLock;
#[derive(Debug)]
pub enum ProcessIdentityError {
MalformedStat {
pid: i64,
},
Io {
pid: i64,
error: std::io::Error,
},
UnsupportedPlatform,
CurrentUnavailable,
ProcMismatch {
procfs: i64,
own: u32,
},
BootUnavailable,
ScopeUnavailable,
}
impl std::fmt::Display for ProcessIdentityError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MalformedStat { pid } => {
return write!(
formatter,
"the Linux process record of PID {pid} is malformed"
);
}
Self::Io { pid, error } => {
return write!(
formatter,
"the process record of PID {pid} could not be read: {error}"
);
}
Self::UnsupportedPlatform => {
return write!(
formatter,
"this platform has no process birth identity source"
);
}
Self::CurrentUnavailable => {
return write!(
formatter,
"the birth identity of the current process is unavailable"
);
}
Self::BootUnavailable => {
return write!(
formatter,
"the boot ID of this host cannot be read from /proc/sys/kernel/random/boot_id, so a lock could not be told from one of an earlier boot"
);
}
Self::ScopeUnavailable => {
return write!(
formatter,
"the PID namespace of this process cannot be read from /proc/self/ns/pid, so a lock could not say which numbering its process number belongs to"
);
}
Self::ProcMismatch { procfs, own } => {
return write!(
formatter,
"the /proc this process sees describes another PID namespace: it reports PID {procfs} for the process that has PID {own}"
);
}
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct MalformedStat;
const EXITED_LINUX_STATES: [&str; 2] = ["Z", "X"];
const LINUX_START_FIELD_INDEX: usize = 19;
pub fn linux_identity_from_stat(stat: &str) -> Result<Option<String>, MalformedStat> {
let Some(command_end) = stat.rfind(')') else {
return Err(MalformedStat);
};
let after: &str = stat.get(command_end + 2..).unwrap_or("");
let fields: Vec<&str> = trim_javascript(after).split(' ').collect();
if EXITED_LINUX_STATES.contains(&fields[0]) {
return Ok(None);
}
match fields.get(LINUX_START_FIELD_INDEX) {
Some(tick) if !tick.is_empty() => return Ok(Some(format!("linux:{tick}"))),
Some(_) | None => return Err(MalformedStat),
}
}
pub fn command_identity(prefix: &str, stdout: &[u8]) -> Option<String> {
let text: std::borrow::Cow<'_, str> = String::from_utf8_lossy(stdout);
let value: &str = trim_javascript(text.as_ref());
if value.is_empty() {
return None;
}
return Some(format!("{prefix}:{value}"));
}
pub fn windows_identity_arguments(pid: i64) -> Vec<String> {
return vec![
String::from("-NoLogo"),
String::from("-NoProfile"),
String::from("-NonInteractive"),
String::from("-Command"),
format!("(Get-Process -Id {pid}).StartTime.ToUniversalTime().Ticks"),
];
}
pub fn darwin_identity_arguments(pid: i64) -> Vec<String> {
return vec![
String::from("-o"),
String::from("lstart="),
String::from("-p"),
pid.to_string(),
];
}
#[cfg_attr(
all(target_os = "linux", not(test)),
allow(
dead_code,
reason = "the release build uses it on macOS and Windows only"
)
)]
fn inspector_identity(command: &str, arguments: &[String], prefix: &str) -> Option<String> {
let output: std::process::Output = std::process::Command::new(command)
.args(arguments)
.env("LC_ALL", "C")
.stdin(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.output()
.ok()?;
if !output.status.success() {
return None;
}
return command_identity(prefix, output.stdout.as_slice());
}
#[cfg(target_os = "linux")]
pub(crate) fn record_is_gone(error: &std::io::Error) -> bool {
return error.kind() == std::io::ErrorKind::NotFound || error.raw_os_error() == Some(libc::ESRCH);
}
#[cfg(target_os = "linux")]
fn linux_identity(pid: i64) -> Result<Option<String>, ProcessIdentityError> {
return linux_identity_read_by(pid, std::fs::read::<String>);
}
#[cfg(target_os = "linux")]
pub(crate) fn linux_identity_read_by(
pid: i64,
read_record: impl FnOnce(String) -> std::io::Result<Vec<u8>>,
) -> Result<Option<String>, ProcessIdentityError> {
let bytes: Vec<u8> = match read_record(format!("/proc/{pid}/stat")) {
Ok(read) => read,
Err(error) if record_is_gone(&error) => return Ok(None),
Err(error) => return Err(ProcessIdentityError::Io { pid, error }),
};
let stat: std::borrow::Cow<'_, str> = String::from_utf8_lossy(bytes.as_slice());
match linux_identity_from_stat(stat.as_ref()) {
Ok(identity) => return Ok(identity),
Err(MalformedStat) => return Err(ProcessIdentityError::MalformedStat { pid }),
}
}
pub fn process_birth_identity(pid: i64) -> Result<Option<String>, ProcessIdentityError> {
#[cfg(target_os = "linux")]
{
return linux_identity(pid);
}
#[cfg(target_os = "macos")]
{
return Ok(inspector_identity(
"ps",
darwin_identity_arguments(pid).as_slice(),
"darwin",
));
}
#[cfg(windows)]
{
return Ok(inspector_identity(
"powershell.exe",
windows_identity_arguments(pid).as_slice(),
"win32",
));
}
#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
{
let _ = pid;
return Err(ProcessIdentityError::UnsupportedPlatform);
}
}
static CURRENT_IDENTITY: OnceLock<String> = OnceLock::new();
pub fn current_birth_identity() -> Result<String, ProcessIdentityError> {
if let Some(known) = CURRENT_IDENTITY.get() {
return Ok(known.clone());
}
let computed: String = process_birth_identity(i64::from(std::process::id()))?
.ok_or(ProcessIdentityError::CurrentUnavailable)?;
let _ = CURRENT_IDENTITY.set(computed.clone());
return Ok(computed);
}
#[cfg(unix)]
pub fn signal_probe(pid: i64) -> std::io::Result<bool> {
let Ok(target) = libc::pid_t::try_from(pid) else {
return Ok(false);
};
if target <= 0 {
return Ok(false);
}
let status: libc::c_int = unsafe { libc::kill(target, 0) };
if status == 0 {
return Ok(true);
}
let error: std::io::Error = std::io::Error::last_os_error();
if error.raw_os_error() == Some(libc::ESRCH) {
return Ok(false);
}
return Err(error);
}
#[cfg(test)]
#[path = "process_identity_tests.rs"]
mod tests;