use super::fs_mount_options::read_mount_facts;
use super::pid_namespace::current_pid_namespace;
use super::process_start::{ProcessStart, linux_start, now_ms};
use super::process_use::{UseScan, UseTarget, processes_using_through};
use super::stale_ref_lock_found::PidFile;
use super::stopped_git_locks::pid_named_by;
use super::stale_ref_lock_walk::pid_file_beside;
pub use super::stale_ref_lock_identity::{Identity, identity_of};
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
pub const MARGIN: Duration = Duration::from_secs(2);
const INITIAL_PID_NAMESPACE: &str = "pid:[4026531836]";
const LOCAL_FILE_SYSTEMS: [&str; 8] = ["btrfs", "ext4", "xfs", "f2fs", "tmpfs", "overlay", "zfs", "bcachefs"];
pub const BOOT_SLACK: Duration = Duration::from_secs(3600);
const REUSE_SLACK_MS: f64 = 100.0;
const GIT_COMMAND_PREFIX: &str = "git";
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Proof {
PidGone(i64),
PidReused {
pid: i64,
command: String,
},
BeforeBoot,
OrphanStaging,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Verdict {
TooNew,
Held(String),
Unproven(String),
Stale(Proof),
}
#[derive(Clone, Debug)]
pub struct Judge {
pub proc_root: PathBuf,
pub margin: Duration,
pub pid_view_complete: bool,
pub boot: Option<SystemTime>,
pub local_file_systems: Vec<String>,
}
#[derive(Clone, Debug)]
pub struct Examined {
pub verdict: Verdict,
pub pid_file: PidFile,
pub identity: Identity,
pub born: Option<SystemTime>,
}
impl Judge {
pub fn here() -> Self {
return Self {
proc_root: PathBuf::from("/proc"),
margin: MARGIN,
pid_view_complete: current_pid_namespace().as_deref() == Some(INITIAL_PID_NAMESPACE),
boot: boot_time(),
local_file_systems: LOCAL_FILE_SYSTEMS.iter().map(|name| { return String::from(*name); }).collect(),
};
}
fn local_check(&self, path: &Path) -> Result<(), String> {
let facts = read_mount_facts(path)?;
let Some(mount) = facts else {
return Err(String::from("the file system that holds it is not in the mount table"));
};
if self.local_file_systems.contains(&mount.filesystem_type) {
return Ok(());
}
return Err(format!("it sits on a `{}` file system, which a Git on another machine may share", mount.filesystem_type));
}
fn pid_file_state(lock: &Path, lock_born: Option<SystemTime>) -> PidFile {
let pid_path: PathBuf = pid_file_beside(lock);
let Some(pid) = pid_named_by(pid_path.as_path()) else {
return PidFile::Absent;
};
let pid_born: Option<SystemTime> = std::fs::symlink_metadata(pid_path.as_path()).ok().and_then(|metadata| { return metadata.created().ok(); });
return match (lock_born, pid_born) {
(Some(lock_at), Some(pid_at)) if pid_at >= lock_at => PidFile::Names(i64::from(pid)),
_ => PidFile::Older(i64::from(pid)),
};
}
fn judge_pid(&self, pid: i64, lock_born: Option<SystemTime>) -> Verdict {
if !self.pid_view_complete {
return Verdict::Unproven(format!(
"the pid file beside it names process {pid}, but this process runs in a PID namespace of its own and cannot see every process of the machine, so it cannot tell whether {pid} is gone"
));
}
return match linux_start(pid, self.proc_root.as_path(), now_ms()) {
ProcessStart::Missing => Verdict::Stale(Proof::PidGone(pid)),
ProcessStart::Unknown(reason) => Verdict::Unproven(format!("the pid file beside it names process {pid}, which could not be looked at: {reason}")),
ProcessStart::Running { started_at_ms, resolution_ms } => {
let command: String = std::fs::read_to_string(self.proc_root.join(pid.to_string()).join("comm")).map(|text| { return text.trim().to_owned(); }).unwrap_or_default();
let born_ms: Option<f64> = lock_born.and_then(|at| { return at.duration_since(SystemTime::UNIX_EPOCH).ok(); }).map(|elapsed| { return elapsed.as_secs_f64() * 1000.0; });
let started_after: bool = born_ms.is_some_and(|born| { return started_at_ms > born + resolution_ms as f64 + REUSE_SLACK_MS; });
if command.starts_with(GIT_COMMAND_PREFIX) || !started_after {
Verdict::Held(format!("process {pid} ({command}), which the pid file beside it names, is running"))
} else {
Verdict::Stale(Proof::PidReused { pid, command })
}
}
};
}
fn before_boot(&self, born: Option<SystemTime>, identity: &Identity) -> bool {
let Some(boot) = self.boot else {
return false;
};
let modified: SystemTime = nanoseconds_to_time(identity.modified_ns);
let slack: Duration = BOOT_SLACK.max(self.margin);
return born.is_some_and(|at| { return at + slack < boot; }) && modified + slack < boot;
}
fn unreadable_git(&self, scan: &UseScan) -> Option<String> {
for reason in &scan.unreadable {
let number: Option<&str> = reason.strip_prefix("PID ").and_then(|rest| { return rest.split(' ').next(); });
let command: Option<String> = number.and_then(|pid| { return std::fs::read_to_string(self.proc_root.join(pid).join("comm")).ok(); });
match (number, command) {
(Some(_), None) => {}
(_, Some(name)) if !name.trim().starts_with(GIT_COMMAND_PREFIX) => {}
_ => return Some(reason.clone()),
}
}
return None;
}
pub fn examine_staging(&self, staging: &Path) -> Option<Examined> {
let identity: Identity = identity_of(staging)?;
let born: Option<SystemTime> = std::fs::symlink_metadata(staging).ok()?.created().ok();
let finish = |verdict: Verdict| { return Some(Examined { verdict, pid_file: PidFile::Absent, identity, born }); };
let newest: SystemTime = born.map_or(nanoseconds_to_time(identity.modified_ns), |at| { return at.max(nanoseconds_to_time(identity.modified_ns)); });
if SystemTime::now().duration_since(newest).unwrap_or_default() < self.margin {
return finish(Verdict::TooNew);
}
if let Err(reason) = self.local_check(staging) {
return finish(Verdict::Unproven(reason));
}
let scan: UseScan = processes_using_through(self.proc_root.as_path(), &UseTarget::File { device: identity.device, inode: identity.inode }, std::process::id());
if let Some(reason) = scan.unavailable.as_ref() {
return finish(Verdict::Unproven(format!("the process table could not be listed: {reason}")));
}
if let Some(user) = scan.users.first() {
return finish(Verdict::Held(format!("process {} ({}) has the file open", user.pid, user.command.clone().unwrap_or_default())));
}
return finish(Verdict::Stale(Proof::OrphanStaging));
}
pub fn examine(&self, lock: &Path) -> Option<Examined> {
let identity: Identity = identity_of(lock)?;
let metadata: std::fs::Metadata = std::fs::symlink_metadata(lock).ok()?;
let born: Option<SystemTime> = metadata.created().ok();
let pid_file: PidFile = Self::pid_file_state(lock, born);
let finish = |verdict: Verdict| { return Some(Examined { verdict, pid_file: pid_file.clone(), identity, born }); };
if !metadata.is_file() {
return finish(Verdict::TooNew);
}
let newest: SystemTime = born.map_or(nanoseconds_to_time(identity.modified_ns), |at| { return at.max(nanoseconds_to_time(identity.modified_ns)); });
if SystemTime::now().duration_since(newest).unwrap_or_default() < self.margin {
return finish(Verdict::TooNew);
}
if let Err(reason) = self.local_check(lock) {
return finish(Verdict::Unproven(reason));
}
let scan: UseScan = processes_using_through(self.proc_root.as_path(), &UseTarget::File { device: identity.device, inode: identity.inode }, std::process::id());
if let Some(reason) = scan.unavailable {
return finish(Verdict::Unproven(format!("the process table could not be listed: {reason}")));
}
if let Some(user) = scan.users.first() {
return finish(Verdict::Held(format!("process {} ({}) has the file open", user.pid, user.command.clone().unwrap_or_default())));
}
if let Some(reason) = self.unreadable_git(&scan) {
return finish(Verdict::Unproven(format!("a Git process of this account could not be looked into ({reason}), so it may hold the lock")));
}
if let PidFile::Names(pid) = pid_file {
return finish(self.judge_pid(pid, born));
}
if self.before_boot(born, &identity) {
return finish(Verdict::Stale(Proof::BeforeBoot));
}
let why: String = match pid_file {
PidFile::Older(pid) => format!("the pid file beside it (process {pid}) is older than the lock, so it belongs to an earlier holder"),
_ => String::from("no pid file beside it names a holder (Git writes one only under `core.lockfilePid`), and no process has it open"),
};
return finish(Verdict::Unproven(why));
}
}
fn nanoseconds_to_time(nanoseconds: i128) -> SystemTime {
let magnitude: u64 = u64::try_from(nanoseconds.unsigned_abs()).unwrap_or(u64::MAX);
return if nanoseconds >= 0 { SystemTime::UNIX_EPOCH + Duration::from_nanos(magnitude) } else { SystemTime::UNIX_EPOCH - Duration::from_nanos(magnitude) };
}
fn boot_time() -> Option<SystemTime> {
let text: String = std::fs::read_to_string("/proc/stat").ok()?;
let seconds: u64 = text.lines().find_map(|line| { return line.strip_prefix("btime ")?.trim().parse::<u64>().ok(); })?;
return Some(SystemTime::UNIX_EPOCH + Duration::from_secs(seconds));
}