use crate::service_index_marker::{Marked, holder_is_elsewhere, holder_is_gone, holder_is_running, 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::{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,
Foreign { file: PathBuf, marked: Marked },
Unjudged { file: PathBuf, marked: Option<Marked> },
Unavailable(std::io::Error),
}
#[derive(Debug, Eq, PartialEq)]
enum Slot {
Free,
Dead,
Live,
Unjudged(Option<Marked>),
Elsewhere(Marked),
}
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 {
use std::os::unix::ffi::OsStrExt;
let mut input: Vec<u8> = lock.file_name().map_or_else(Vec::new, |name| { return name.as_bytes().to_vec(); });
input.push(0);
input.extend_from_slice(key.as_bytes());
let digest: [u8; 32] = crate::subvolume_sha::sha256(input.as_slice());
let hex: String = digest.iter().take(CLAIM_DIGEST_BYTES).map(|byte| { return format!("{byte:02x}"); }).collect();
return lock.with_file_name(format!("{CLAIM_INFIX}{hex}-{slot}"));
}
const CLAIM_DIGEST_BYTES: usize = 16;
pub fn claim_key(marked: &Marked) -> String {
return match marked.holder.as_ref() {
Some((pid, birth)) => {
let escaped: String = birth.bytes().map(|byte| { return if matches!(byte, b'/' | 0 | b'%') { format!("%{byte:02x}") } else { String::from(char::from(byte)) }; }).collect();
format!("{:x}-{pid:x}-{escaped}", marked.seq)
}
None => format!("{:x}-n", marked.seq),
};
}
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 not judged", path.display());
return Slot::Unjudged(None);
}
};
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 not judged", path.display());
return Slot::Unjudged(None);
}
}
return match parse_marker(text.as_str()) {
Some(marked) if holder_is_gone(marked.holder.as_ref()) => Slot::Dead,
Some(marked) if holder_is_elsewhere(marked.holder.as_ref()) => Slot::Elsewhere(marked),
Some(marked) if holder_is_running(marked.holder.as_ref()) => Slot::Live,
Some(marked) => Slot::Unjudged(Some(marked)),
None => Slot::Dead,
};
}
fn text_of(path: &Path) -> std::io::Result<Option<String>> {
let file = match std::fs::OpenOptions::new().read(true).custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK | libc::O_CLOEXEC).open(path) {
Ok(file) => file,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(error) => return Err(error),
};
let mut text: String = String::new();
return match file.take(CLAIM_READ_LIMIT).read_to_string(&mut text) {
Ok(_) => Ok(Some(text)),
Err(error) if error.kind() == std::io::ErrorKind::InvalidData => Ok(None),
Err(error) => Err(error),
};
}
thread_local! {
static ABANDONED: std::cell::RefCell<Vec<(PathBuf, String)>> = const { std::cell::RefCell::new(Vec::new()) };
}
fn resolve_abandoned() {
let pending: Vec<(PathBuf, String)> = ABANDONED.with(|list| { return std::mem::take(&mut *list.borrow_mut()); });
for (path, text) in pending {
match text_of(path.as_path()) {
Ok(Some(found)) if found == text => remove_claim(path.as_path()),
Ok(_) => {}
Err(_) => ABANDONED.with(|list| { list.borrow_mut().push((path, 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, marked: &Marked) -> Claim {
let key: String = claim_key(marked);
resolve_abandoned();
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(_) => {
crate::service_fault::reach("reclaim-claim-unread");
match text_of(path.as_path()) {
Ok(found) => found.is_some_and(|text| { return text == marker; }),
Err(error) => {
crate::service_error!("the claim file {} of a reclaim cannot be read back ({error}); the next attempt resolves it", path.display());
ABANDONED.with(|list| { list.borrow_mut().push((path.clone(), marker.clone())); });
break Claim::Busy;
}
}
}
};
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,
Slot::Elsewhere(theirs) => break Claim::Foreign { file: path, marked: theirs },
Slot::Unjudged(theirs) => break Claim::Unjudged { file: path, marked: theirs },
},
Err(error) if matches!(error.kind(), std::io::ErrorKind::Interrupted | std::io::ErrorKind::WouldBlock | std::io::ErrorKind::OutOfMemory) => 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;
}