use crate::service_index_lock::lock_path_of;
use crate::service_index_claim::{Claim, ClaimHold, take_claim};
use crate::service_index_marker::{LOCK_MARKER, MARKER_VERSION, Marked, holder_is_elsewhere, holder_is_gone, marker_of_file};
use crate::service_index_placement::{Platform, SYSTEM};
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
use std::path::{Path, PathBuf};
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Reclaimed {
Removed,
Changed,
Busy,
Kept,
Stuck,
Unguarded(Marked),
Foreign { file: PathBuf, marked: Marked },
Unjudged { file: PathBuf, marked: Option<Marked> },
}
pub fn remove_stale(index: &Path, seq: u64) {
let lock: PathBuf = lock_path_of(index);
let for_this_batch = |marked: &Marked| -> bool { return marked.seq == seq && holder_is_gone(marked.holder.as_ref()); };
let _outcome: Reclaimed = reclaim_if(&SYSTEM, lock.as_path(), &for_this_batch);
}
pub fn reclaim_if(platform: &Platform, lock: &Path, is_stale: &dyn Fn(&Marked) -> bool) -> Reclaimed {
let file: std::fs::File = match open_to_judge(lock) {
Ok(opened) => opened,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Reclaimed::Removed,
Err(_) => return Reclaimed::Kept,
};
crate::service_fault::reach("reclaim-opened");
let Some(marked) = marker_of_file(&file) else {
return Reclaimed::Kept;
};
if !is_stale(&marked) {
if holder_is_elsewhere(marked.holder.as_ref()) {
crate::service_error!("{}", foreign_words(lock, &marked));
return Reclaimed::Foreign { file: lock.to_path_buf(), marked };
}
return Reclaimed::Kept;
}
if !marked.current {
crate::service_error!("{}", foreign_words(lock, &marked));
return Reclaimed::Foreign { file: lock.to_path_buf(), marked };
}
crate::service_fault::reach("reclaim-judged");
let claim: ClaimHold = match take_claim(platform, lock, &marked) {
Claim::Held(held) => held,
Claim::Busy => return Reclaimed::Busy,
Claim::Unjudged { file: in_the_way, marked: theirs } => {
crate::service_error!("{}", unjudged_words(in_the_way.as_path(), theirs.as_ref()));
return Reclaimed::Unjudged { file: in_the_way, marked: theirs };
}
Claim::Foreign { file: in_the_way, marked: theirs } => {
crate::service_error!("{}", foreign_words(in_the_way.as_path(), &theirs));
return Reclaimed::Foreign { file: in_the_way, marked: theirs };
}
Claim::Unavailable(why) => {
crate::service_error!("the index lock {} was left by a service that is gone, and it is not removed: no claim file could be placed beside it to keep two reclaimers apart ({why}), so a removal could take a live lock; \
remove it by hand once no git command runs in this repository",
lock.display()
);
return Reclaimed::Unguarded(marked);
}
};
crate::service_fault::reach("reclaim-held");
match same_file(lock, &file) {
Ok(true) => {}
Ok(false) => return cleared(claim, Reclaimed::Changed),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return cleared(claim, Reclaimed::Removed),
Err(error) => {
crate::service_error!("the index lock {} was left by a service that is gone, and it cannot be looked at ({error})", lock.display());
return Reclaimed::Stuck;
}
}
return match (platform.unlink)(lock) {
Ok(()) => cleared(claim, Reclaimed::Removed),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => cleared(claim, Reclaimed::Removed),
Err(error) => {
crate::service_error!("the index lock {} was left by a service that is gone, and it cannot be removed ({error}); \
remove it by hand, and clear its immutable attribute or fix the directory's permissions first if that is why it stays",
lock.display()
);
Reclaimed::Stuck
}
};
}
fn cleared(claim: ClaimHold, answer: Reclaimed) -> Reclaimed {
claim.clear_all();
return answer;
}
fn open_to_judge(lock: &Path) -> std::io::Result<std::fs::File> {
return std::fs::OpenOptions::new().read(true).custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK | libc::O_CLOEXEC).open(lock);
}
fn same_file(lock: &Path, file: &std::fs::File) -> std::io::Result<bool> {
let named = std::fs::symlink_metadata(lock)?;
let open = file.metadata()?;
return Ok(named.dev() == open.dev() && named.ino() == open.ino());
}
pub fn shell_quote(path: &Path) -> String {
return format!("'{}'", path.display().to_string().replace('\'', "'\\''"));
}
pub fn stale_lock_words(lock: &Path, marked: &Marked) -> String {
let (who, first_line): (String, String) = match marked.holder.as_ref() {
Some((pid, birth)) => (
format!("cli-git-service batch {} (process {pid}, which is gone)", marked.seq),
format!("{LOCK_MARKER}{}{} {pid} {birth}", if marked.current { MARKER_VERSION } else { "" }, marked.seq),
),
None => (format!("cli-git-service batch {}", marked.seq), format!("{LOCK_MARKER}{}", marked.seq)),
};
return format!(
"The index lock {} was left by {who} and is not removed here, because no claim file that makes its removal safe could be placed beside it on this file system. \
The commit waits as for any held lock. Once no git command and no commit of this service is running in this repository, and the first line of the file \
still reads `{first_line}`, remove it yourself (`rm -- {}`).",
lock.display(),
shell_quote(lock)
);
}
pub fn foreign_words(file: &Path, marked: &Marked) -> String {
let who: String = match marked.holder.as_ref() {
Some((pid, _)) => format!("cli-git-service batch {} (process {pid})", marked.seq),
None => format!("cli-git-service batch {}", marked.seq),
};
let why: &str = if marked.current {
"on another machine, or on one this machine cannot compare itself with, so that service is not proven gone"
} else {
"in the format of an earlier release, whose own stale-lock removal cannot keep this release's apart from it and whose marker \
does not say which machine wrote it, so this release does not remove it"
};
return format!(
"The file {} was written by {who} {why}. The commit waits as for any held lock. \
If that service is not running and no git command and no commit of any release's service runs in this repository, remove it yourself (`rm -- {}`).",
file.display(),
shell_quote(file)
);
}
pub fn unjudged_words(file: &Path, marked: Option<&Marked>) -> String {
let who: String = match marked.and_then(|found| { return found.holder.as_ref().map(|(pid, _)| { return (found.seq, *pid); }); }) {
Some((seq, pid)) => format!("made by cli-git-service batch {seq} (process {pid}) "),
None => String::new(),
};
return format!(
"The claim file {} {who}cannot be judged here (its maker is in another PID namespace, or the file cannot be read), so it is neither removed nor passed over, and it keeps the stale index lock from being reclaimed. \
The commit waits as for any held lock. If that reclaimer is gone and no git command and no commit of any service runs in this repository, remove it yourself (`rm -- {}`).",
file.display(),
shell_quote(file)
);
}