use crate::claim_time::{current_boot_id, current_machine_id, is_machine_id};
use crate::process_identity::{ProcessIdentityError, current_birth_identity, process_birth_identity};
use crate::service_index_marker::marker_of;
use std::path::Path;
use std::sync::OnceLock;
pub const BOOT_SEPARATOR: char = '#';
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HolderState {
Gone,
Running,
Unknown,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ReaderView<'a> {
pub boot: &'a str,
pub scope: Option<&'a str>,
pub machine: Option<&'a str>,
pub procfs_agrees: bool,
}
pub fn scope_of_link(text: &str) -> Option<String> {
let number: &str = text.strip_prefix("pid:[")?.strip_suffix(']')?;
return (!number.is_empty() && number.as_bytes().iter().all(u8::is_ascii_digit)).then_some(number.to_string());
}
pub fn current_pid_scope() -> Option<&'static str> {
static SCOPE: OnceLock<Option<String>> = OnceLock::new();
return SCOPE
.get_or_init(read_pid_scope)
.as_deref();
}
pub fn procfs_agrees_with(read: std::io::Result<Vec<u8>>, own: u32) -> bool {
let Ok(stat) = read else { return false };
return check_procfs_view(stat.as_slice(), own).is_ok();
}
fn current_procfs_agrees() -> bool {
static AGREES: OnceLock<bool> = OnceLock::new();
return *AGREES.get_or_init(read_procfs_agreement);
}
fn read_procfs_agreement() -> bool {
return procfs_agrees_with(std::fs::read("/proc/self/stat"), std::process::id());
}
impl ReaderView<'static> {
pub fn current() -> ReaderView<'static> {
return ReaderView { boot: current_boot_id(), scope: current_pid_scope(), machine: non_empty(current_machine_id()), procfs_agrees: current_procfs_agrees() };
}
}
fn read_pid_scope() -> Option<String> {
let target = std::fs::read_link("/proc/self/ns/pid").ok()?;
return scope_of_link(target.to_string_lossy().as_ref());
}
fn non_empty(text: &str) -> Option<&str> {
return if text.is_empty() { None } else { Some(text) };
}
fn is_space(byte: &u8) -> bool {
return *byte == b' ';
}
pub fn pid_of_self_stat(stat: &[u8]) -> Option<i64> {
let digits: &[u8] = stat.split(is_space).next()?;
return std::str::from_utf8(digits).ok()?.parse::<i64>().ok();
}
pub fn own_identity() -> Result<String, ProcessIdentityError> {
let own: u32 = std::process::id();
#[cfg(target_os = "linux")]
let stat: Vec<u8> = match std::fs::read("/proc/self/stat") {
Ok(bytes) => bytes,
Err(error) => return Err(ProcessIdentityError::Io { pid: i64::from(own), error }),
};
#[cfg(not(target_os = "linux"))]
let stat: Vec<u8> = format!("{own} ").into_bytes();
return identity_in_view_on(stat.as_slice(), own, current_boot_id(), current_pid_scope(), non_empty(current_machine_id()));
}
pub fn identity_in_view(stat: &[u8], own: u32, boot: &str, scope: Option<&str>) -> Result<String, ProcessIdentityError> {
return identity_in_view_on(stat, own, boot, scope, None);
}
pub fn identity_in_view_on(stat: &[u8], own: u32, boot: &str, scope: Option<&str>, machine: Option<&str>) -> Result<String, ProcessIdentityError> {
check_procfs_view(stat, own)?;
let birth: String = current_birth_identity()?;
#[cfg(target_os = "linux")]
{
if boot.is_empty() {
return Err(ProcessIdentityError::BootUnavailable);
}
if scope.is_none() {
return Err(ProcessIdentityError::ScopeUnavailable);
}
}
let mut identity: String = birth;
let machine_part: Option<&str> = match scope.and(machine) {
Some(id) if is_machine_id(id) => Some(id),
_ => None,
};
for part in [non_empty(boot), scope, machine_part] {
if let Some(text) = part {
identity.push(BOOT_SEPARATOR);
identity.push_str(text);
}
}
return Ok(identity);
}
pub fn check_procfs_view(stat: &[u8], own: u32) -> Result<(), ProcessIdentityError> {
let procfs: i64 = pid_of_self_stat(stat).ok_or(ProcessIdentityError::MalformedStat { pid: i64::from(own) })?;
if procfs != i64::from(own) {
return Err(ProcessIdentityError::ProcMismatch { procfs, own });
}
return Ok(());
}
fn split_birth(birth: &str) -> (&str, Option<&str>, Option<&str>, Option<&str>) {
let mut parts = birth.splitn(4, BOOT_SEPARATOR);
let identity: &str = parts.next().unwrap_or(birth);
return (identity, parts.next(), parts.next(), parts.next());
}
pub fn holder_state_in(
holder: Option<&(i64, String)>,
view: &ReaderView,
ask: &dyn Fn(i64) -> Result<Option<String>, ProcessIdentityError>,
) -> HolderState {
let Some((pid, birth)) = holder else { return HolderState::Unknown };
let (identity, boot, scope, machine) = split_birth(birth.as_str());
if let Some(recorded) = machine
&& (!is_machine_id(recorded) || view.machine != Some(recorded))
{
return HolderState::Unknown;
}
if machine.is_none()
&& view.machine.is_some()
&& matches!(boot, Some(recorded) if !view.boot.is_empty() && recorded != view.boot)
{
return HolderState::Unknown;
}
if let Some(recorded) = boot
&& !view.boot.is_empty()
&& recorded != view.boot
{
return HolderState::Gone;
}
if !view.procfs_agrees || (boot.is_some() && view.boot.is_empty()) {
return HolderState::Unknown;
}
if let Some(recorded) = scope
&& view.scope != Some(recorded)
{
return HolderState::Unknown;
}
return match ask(*pid) {
Ok(None) => HolderState::Gone,
Ok(Some(now)) if now == identity => HolderState::Running,
Ok(Some(_)) => HolderState::Gone,
Err(_) => HolderState::Unknown,
};
}
pub fn holder_is_elsewhere_in(holder: Option<&(i64, String)>, view: &ReaderView) -> bool {
let Some((_, birth)) = holder else { return true };
let (_, boot, _, machine) = split_birth(birth.as_str());
return match machine {
Some(recorded) => !is_machine_id(recorded) || view.machine != Some(recorded),
None => view.machine.is_some() && matches!(boot, Some(recorded) if !view.boot.is_empty() && recorded != view.boot),
};
}
pub fn holder_is_elsewhere(holder: Option<&(i64, String)>) -> bool {
return holder_is_elsewhere_in(holder, &ReaderView::current());
}
pub fn holder_state(holder: Option<&(i64, String)>) -> HolderState {
return holder_state_in(holder, &ReaderView::current(), &process_birth_identity);
}
pub fn holder_is_gone(holder: Option<&(i64, String)>) -> bool {
return holder_state(holder) == HolderState::Gone;
}
pub fn holder_is_running(holder: Option<&(i64, String)>) -> bool {
return holder_state(holder) == HolderState::Running;
}
pub fn mark_names_a_gone_holder(lock: &Path) -> bool {
return matches!(marker_of(lock), Some(marked) if marked.holder.is_some() && holder_is_gone(marked.holder.as_ref()));
}