use crate::service_index_marker::{Marked, holder_is_gone, marker_text, parse_marker};
use crate::service_index_placement::{Platform, place_marked};
use crate::service_index_staging::stage_marker;
use std::io::Read;
use std::os::unix::fs::{MetadataExt, OpenOptionsExt, PermissionsExt};
use std::path::{Path, PathBuf};
pub const CLAIM_INFIX: &str = ".cli-reclaim-";
const CLAIM_READ_LIMIT: u64 = 512;
const CLAIM_MODE: u32 = 0o644;
pub enum Claim {
Held(ClaimHold),
Busy,
Unavailable(std::io::Error),
}
#[derive(Debug, Eq, PartialEq)]
enum Slot {
Free,
Dead,
Live,
}
pub struct ClaimHold {
lock: PathBuf,
key: String,
own: usize,
cleared: bool,
}
impl ClaimHold {
pub fn clear_all(mut self) {
self.cleared = true;
for slot in 0..=self.own {
remove_claim(claim_path(&self.lock, self.key.as_str(), slot).as_path());
}
}
}
impl Drop for ClaimHold {
fn drop(&mut self) {
if !self.cleared {
remove_claim(claim_path(&self.lock, self.key.as_str(), self.own).as_path());
}
}
}
pub fn claim_path(lock: &Path, key: &str, slot: usize) -> PathBuf {
let mut name = lock.as_os_str().to_os_string();
name.push(format!("{CLAIM_INFIX}{key}-{slot}"));
return PathBuf::from(name);
}
pub fn claim_key(stale: &std::fs::File, marked: &Marked) -> std::io::Result<String> {
return match marked.holder.as_ref() {
Some((pid, birth)) => {
let hex: String = birth.bytes().map(|byte| { return format!("{byte:02x}"); }).collect();
Ok(format!("{:x}-{pid:x}-{hex}", marked.seq))
}
None => {
let identity = stale.metadata()?;
Ok(format!("{:x}-n-{:x}-{:x}", marked.seq, identity.dev(), identity.ino()))
}
};
}
fn remove_claim(path: &Path) {
match std::fs::remove_file(path) {
Ok(()) => {}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => crate::service_error!("the claim file {} of a reclaim cannot be removed ({error}); it is inert, remove it by hand", path.display()),
}
}
fn look_at(path: &Path) -> Slot {
let opened = std::fs::OpenOptions::new().read(true).custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK | libc::O_CLOEXEC).open(path);
let file = match opened {
Ok(file) => file,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Slot::Free,
Err(error) => {
crate::service_error!("the claim file {} of a reclaim cannot be opened ({error}), so its maker is taken to run", path.display());
return Slot::Live;
}
};
let mut text: String = String::new();
match file.take(CLAIM_READ_LIMIT).read_to_string(&mut text) {
Ok(_) => {}
Err(error) if error.kind() == std::io::ErrorKind::InvalidData => return Slot::Dead,
Err(error) => {
crate::service_error!("the claim file {} of a reclaim cannot be read ({error}), so its maker is taken to run", path.display());
return Slot::Live;
}
}
return match parse_marker(text.as_str()) {
Some(marked) if !holder_is_gone(marked.holder.as_ref()) => Slot::Live,
_ => Slot::Dead,
};
}
fn text_of(path: &Path) -> Option<String> {
let file = std::fs::OpenOptions::new().read(true).custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK | libc::O_CLOEXEC).open(path).ok()?;
let mut text: String = String::new();
file.take(CLAIM_READ_LIMIT).read_to_string(&mut text).ok()?;
return Some(text);
}
fn attempt_token() -> std::io::Result<u64> {
let uuid: String = crate::random_id::random_uuid().map_err(|error| { return std::io::Error::other(error.0); })?;
let digits: String = uuid.chars().filter(char::is_ascii_hexdigit).take(16).collect();
return u64::from_str_radix(digits.as_str(), 16).map_err(std::io::Error::other);
}
fn all_below_are_dead(lock: &Path, key: &str, slot: usize) -> bool {
return (0..slot).all(|below| { return look_at(claim_path(lock, key, below).as_path()) == Slot::Dead; });
}
pub fn take_claim(platform: &Platform, lock: &Path, stale: &std::fs::File, marked: &Marked) -> Claim {
let key: String = match claim_key(stale, marked) {
Ok(key) => key,
Err(error) => return Claim::Unavailable(error),
};
let token: u64 = match attempt_token() {
Ok(token) => token,
Err(error) => return Claim::Unavailable(error),
};
let marker: String = match marker_text(token, platform.identity) {
Ok(text) => text,
Err(error) => return Claim::Unavailable(std::io::Error::other(format!("this process has no birth identity to name in a claim ({error})"))),
};
let (staged, open) = match stage_marker(lock, token, marker.as_str()) {
Ok(made) => made,
Err(error) => return Claim::Unavailable(error),
};
if let Err(error) = open.set_permissions(std::fs::Permissions::from_mode(CLAIM_MODE)) {
let _gone = std::fs::remove_file(staged.as_path());
return Claim::Unavailable(error);
}
let mut slot: usize = 0;
let outcome: Claim = loop {
let path: PathBuf = claim_path(lock, key.as_str(), slot);
let placed: std::io::Result<()> = place_marked(platform, staged.as_path(), path.as_path());
let ours: bool = match &placed {
Ok(()) => true,
Err(_) => text_of(path.as_path()).is_some_and(|text| { return text == marker; }),
};
if ours {
crate::service_fault::reach("reclaim-claim-placed");
let hold = ClaimHold { lock: lock.to_path_buf(), key: key.clone(), own: slot, cleared: false };
if all_below_are_dead(lock, key.as_str(), slot) {
break Claim::Held(hold);
}
drop(hold);
break Claim::Busy;
}
match placed {
Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => match look_at(path.as_path()) {
Slot::Dead => slot += 1,
Slot::Live | Slot::Free => break Claim::Busy,
},
Err(error) => break Claim::Unavailable(error),
Ok(()) => unreachable!("a placement that succeeded is the attempt's own claim"),
}
};
let _gone = std::fs::remove_file(staged.as_path());
return outcome;
}