use crate::service_index_lock::lock_path_of;
use crate::service_index_marker::{LOCK_MARKER, Marked, holder_is_gone, marker_of_file};
use crate::service_index_placement::{Platform, SYSTEM};
use std::os::fd::AsRawFd;
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),
}
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) {
return Reclaimed::Kept;
}
crate::service_fault::reach("reclaim-judged");
match hold(platform, &file) {
Hold::Taken => {}
Hold::Busy => return Reclaimed::Busy,
Hold::Unavailable(error) => {
crate::service_error!("the index lock {} was left by a service that is gone, and it is not removed: the hold that keeps two reclaimers apart could not be taken ({error}), 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 Reclaimed::Changed,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return 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(()) => Reclaimed::Removed,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => 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 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);
}
enum Hold {
Taken,
Busy,
Unavailable(std::io::Error),
}
fn hold(platform: &Platform, file: &std::fs::File) -> Hold {
return match (platform.flock)(file) {
Ok(()) => Hold::Taken,
Err(error) if matches!(error.kind(), std::io::ErrorKind::WouldBlock | std::io::ErrorKind::Interrupted) => Hold::Busy,
Err(error) => Hold::Unavailable(error),
};
}
pub fn flock_exclusive(file: &std::fs::File) -> std::io::Result<()> {
let result: libc::c_int = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
return if result == 0 { Ok(()) } else { Err(std::io::Error::last_os_error()) };
}
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 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}", 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 the file lock that makes its removal safe could not be taken 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(),
lock.display()
);
}