use crate::index_lock_holders::{HolderEvidence, scan_platform_holders};
use crate::process_start::{ProcessStart, resolve_process_start};
use crate::recovery_files::lock_pid_path;
use std::path::{Path, PathBuf};
const QUOTED_TEXT_LIMIT: usize = 64;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct LockMetadata {
pub device: u64,
pub inode: u64,
pub ctime_ms: i64,
}
#[derive(Clone, Debug, PartialEq)]
pub enum PidOwner {
Missing,
StartedBeforeLock(
f64,
),
StartedAfterLock(
f64,
),
StartUnknown(
String,
),
}
#[derive(Clone, Debug, PartialEq)]
pub enum PidFile {
Absent,
Malformed(
String,
),
Unreadable(
String,
),
Owner(
i64,
PidOwner,
),
}
#[derive(Clone, Debug, PartialEq)]
pub struct Evidence {
pub lock_path: PathBuf,
pub lock: LockMetadata,
pub pid_file: PidFile,
pub holders: Option<HolderEvidence>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Verdict {
ProvenAlive {
pid: i64,
by_descriptor: bool,
command: Option<String>,
},
Dead(
i64,
),
EvidenceFree,
}
pub fn read_lock_metadata(lock_path: &Path) -> std::io::Result<Option<LockMetadata>> {
let metadata: std::fs::Metadata = match std::fs::symlink_metadata(lock_path) {
Ok(found) => found,
Err(error) if is_missing(&error) => return Ok(None),
Err(error) => return Err(error),
};
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
return Ok(Some(LockMetadata {
device: metadata.dev(),
inode: metadata.ino(),
ctime_ms: metadata.ctime() * 1000 + metadata.ctime_nsec() / 1_000_000,
}));
}
#[cfg(not(unix))]
{
let changed: i64 = match metadata.modified() {
Ok(time) => match time.duration_since(std::time::UNIX_EPOCH) {
Ok(elapsed) => i64::try_from(elapsed.as_millis()).unwrap_or(i64::MAX),
Err(_) => 0,
},
Err(_) => 0,
};
return Ok(Some(LockMetadata { device: 0, inode: 0, ctime_ms: changed }));
}
}
fn is_missing(error: &std::io::Error) -> bool {
return matches!(error.kind(), std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory);
}
pub fn parse_pid_file_text(text: &str) -> Option<i64> {
let trimmed: &str = crate::js_text::trim_javascript_end(text);
let digits: &str = trimmed.strip_prefix("pid ")?;
return crate::js_text::decimal_attempt(digits);
}
pub fn owner_evidence(start: ProcessStart, lock: &LockMetadata) -> PidOwner {
match start {
ProcessStart::Missing => return PidOwner::Missing,
ProcessStart::Unknown(reason) => return PidOwner::StartUnknown(reason),
ProcessStart::Running { started_at_ms, resolution_ms } => {
if started_at_ms <= (lock.ctime_ms + resolution_ms) as f64 {
return PidOwner::StartedBeforeLock(started_at_ms);
}
return PidOwner::StartedAfterLock(started_at_ms);
}
}
}
fn quoted_prefix(text: &str) -> String {
let mut prefix: String = String::new();
let mut units: usize = 0;
for character in text.chars() {
units += character.len_utf16();
if units > QUOTED_TEXT_LIMIT {
break;
}
prefix.push(character);
}
return prefix;
}
pub fn read_pid_file_evidence(pid_path: &Path, lock: &LockMetadata, resolve: fn(i64) -> ProcessStart) -> PidFile {
let text: String = match std::fs::read(pid_path) {
Ok(bytes) => String::from_utf8_lossy(bytes.as_slice()).into_owned(),
Err(error) if is_missing(&error) => return PidFile::Absent,
Err(error) => return PidFile::Unreadable(error.to_string()),
};
let Some(pid) = parse_pid_file_text(text.as_str()) else {
return PidFile::Malformed(quoted_prefix(text.as_str()));
};
return PidFile::Owner(pid, owner_evidence(resolve(pid), lock));
}
pub fn classify_index_lock(evidence: &Evidence) -> Verdict {
if let Some(scan) = &evidence.holders
&& let Some(holder) = scan.holders.first()
{
return Verdict::ProvenAlive {
pid: holder.pid,
by_descriptor: true,
command: holder.command.clone(),
};
}
match &evidence.pid_file {
PidFile::Owner(pid, PidOwner::StartedBeforeLock(_)) => {
return Verdict::ProvenAlive {
pid: *pid,
by_descriptor: false,
command: None,
};
}
PidFile::Owner(pid, PidOwner::Missing) => return Verdict::Dead(*pid),
_ => return Verdict::EvidenceFree,
}
}
pub fn gather_with(
real_index: &Path,
resolve: fn(i64) -> ProcessStart,
scan: fn(&Path, u64, u64) -> HolderEvidence,
) -> std::io::Result<Option<Evidence>> {
let mut name: std::ffi::OsString = real_index.as_os_str().to_owned();
name.push(".lock");
let lock_path: PathBuf = PathBuf::from(name);
let Some(lock) = read_lock_metadata(lock_path.as_path())? else {
return Ok(None);
};
let pid_file: PidFile = read_pid_file_evidence(lock_pid_path(real_index).as_path(), &lock, resolve);
if let PidFile::Owner(pid, PidOwner::StartedBeforeLock(_)) = &pid_file {
crate::diagnostic_log::debug(
"gatherIndexLockEvidence",
format!("PID file proves live owner {pid}").as_str(),
);
return Ok(Some(Evidence { lock_path, lock, pid_file, holders: None }));
}
let holders: HolderEvidence = scan(lock_path.as_path(), lock.device, lock.inode);
return Ok(Some(Evidence {
lock_path,
lock,
pid_file,
holders: Some(holders),
}));
}
pub fn gather_index_lock_evidence(real_index: &Path) -> std::io::Result<Option<Evidence>> {
return gather_with(real_index, resolve_process_start, scan_platform_holders);
}
fn holder_name(pid: i64, command: Option<&str>) -> String {
match command {
Some(name) => return format!("PID {pid} ({name})"),
None => return format!("PID {pid}"),
}
}
pub fn proven_holder_line(pid: i64, by_descriptor: bool, command: Option<&str>, lock_path: &Path) -> String {
let proof: &str = if by_descriptor { "an open descriptor" } else { "its Git lock PID file" };
return format!(
"cli-git: waiting for {}, which holds {} (proven by {proof}); this waits for as long as that process holds the lock \
and leaves the lock in place.\n",
holder_name(pid, command),
lock_path.display()
);
}
pub fn proven_holder_progress_line(pid: i64, by_descriptor: bool, command: Option<&str>, lock_path: &Path, waited_seconds: u64) -> String {
let proof: &str = if by_descriptor { "an open descriptor" } else { "its Git lock PID file" };
return format!(
"cli-git: still waiting for {}, which holds {} (proven by {proof}) after {waited_seconds} seconds; this waits for as \
long as that process holds the lock and leaves the lock in place.\n",
holder_name(pid, command),
lock_path.display()
);
}
fn evidence_words(evidence: &Evidence) -> String {
let descriptors: String = match &evidence.holders {
Some(scan) if scan.partial.is_empty() => String::from("no process has the lock open"),
Some(scan) => format!("no process the scan could see has the lock open (the scan could not see everything: {})", scan.partial.join("; ")),
None => String::from("the open descriptors were not scanned"),
};
let pid_file: String = match &evidence.pid_file {
PidFile::Absent => String::from("no Git lock PID file beside it (`core.lockfilePid` is off for the program that made it, or it is not Git)"),
PidFile::Malformed(text) => format!("a Git lock PID file whose text is not `pid <number>` ({text:?})"),
PidFile::Unreadable(reason) => format!("a Git lock PID file that could not be read ({reason})"),
PidFile::Owner(pid, PidOwner::StartedAfterLock(_)) => format!("a Git lock PID file naming PID {pid}, which started after the lock was made (the number was reused)"),
PidFile::Owner(pid, PidOwner::StartUnknown(reason)) => format!("a Git lock PID file naming PID {pid}, whose start time could not be read ({reason})"),
PidFile::Owner(pid, PidOwner::Missing) => format!("a Git lock PID file naming PID {pid}, which no longer runs"),
PidFile::Owner(pid, PidOwner::StartedBeforeLock(_)) => format!("a Git lock PID file naming PID {pid}, which runs"),
};
return format!("{descriptors}, and {pid_file}");
}
pub fn unproven_wait_line(lock_path: &Path, evidence: Option<&Evidence>) -> String {
let known: String = match evidence {
Some(found) => format!(" ({})", evidence_words(found)),
None => String::from(" (the evidence about its owner could not be read)"),
};
return format!(
"cli-git: waiting for {lock}, whose owner cannot be proven alive{known}; this waits for as long as the lock stays there. \
If no program is using this repository, the lock was left by a program that was killed, and removing the file {lock} \
lets the command run.\n",
lock = lock_path.display()
);
}
pub fn ceiling_line(lock_path: &Path, blocker: &str, waited_seconds: u64, consequence: &str) -> String {
return format!(
"cli-git: {lock} is still held after {waited_seconds} seconds by {blocker}, so {consequence}; nothing was changed and the lock was \
left in place. Run the command again once that program is done.\n",
lock = lock_path.display()
);
}
pub fn blocker_words(evidence: &Evidence) -> String {
return match classify_index_lock(evidence) {
Verdict::ProvenAlive { pid, by_descriptor, command } => {
let proof: &str = if by_descriptor { "an open descriptor" } else { "its Git lock PID file" };
format!("{} (proven by {proof})", holder_name(pid, command.as_deref()))
}
Verdict::Dead(pid) => format!("{}, which no longer runs", holder_name(pid, None)),
Verdict::EvidenceFree => format!("an owner nothing proves alive ({})", evidence_words(evidence)),
};
}
pub fn dead_owner_diagnostic(lock_path: &Path, pid: i64, consequence: &str) -> String {
return format!(
"cli-git: {lock} was left by PID {pid}, which no longer runs, so nothing will release it. {consequence}; nothing was \
changed and the lock was left in place. If no other program is using this repository, removing the file {lock} lets \
the command run.\n",
lock = lock_path.display()
);
}
#[cfg(test)]
#[path = "index_lock_evidence_tests.rs"]
mod tests;