use crate::service_index_lock::{LOCK_MARKER, Watch, lock_path_of};
use crate::service_index_watch::POLL_PAUSE;
use crate::wait_ceilings::SERVICE_HOLD_CEILING;
use crate::service_index_marker::{holder_is_running, mark_names_a_gone_holder, marker_of};
use crate::wait_progress::{Cadence, Ticker};
use std::io::{Read, Write};
use std::path::Path;
use std::time::{Duration, Instant};
const EMPTY_LOOKS: u32 = 5;
const EMPTY_PAUSE: Duration = Duration::from_millis(2);
pub const UNASKABLE_HOLD_AGE: Duration = Duration::from_secs(150);
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Holder {
Nobody,
Service,
Foreign,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum Waited {
NotHeld,
Released,
GaveUp,
Stale,
}
fn holder_of(lock: &Path) -> Holder {
return holder_after_pauses(lock, &|| { std::thread::sleep(EMPTY_PAUSE); });
}
fn holder_after_pauses(lock: &Path, pause: &dyn Fn()) -> Holder {
for look in 0..EMPTY_LOOKS {
let mut file = match std::fs::File::open(lock) {
Ok(file) => file,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Holder::Nobody,
Err(_) => return Holder::Foreign,
};
let mut head: [u8; LOCK_MARKER.len()] = [0; LOCK_MARKER.len()];
let mut filled: usize = 0;
while filled < head.len() {
match file.read(&mut head[filled..]) {
Ok(0) => break,
Ok(count) => filled += count,
Err(_) => return Holder::Foreign,
}
}
if filled == head.len() {
return if head == *LOCK_MARKER.as_bytes() { Holder::Service } else { Holder::Foreign };
}
if filled > 0 && !LOCK_MARKER.as_bytes().starts_with(&head[..filled]) {
return Holder::Foreign;
}
if look + 1 < EMPTY_LOOKS {
pause();
}
}
return Holder::Foreign;
}
pub fn foreign_holds(real_index: &Path) -> bool {
return holder_of(lock_path_of(real_index).as_path()) == Holder::Foreign;
}
fn waiting_line(command: &str, waited: Duration) -> String {
return format!(
"cli-git: git {command} is waiting for the commit service to finish a commit in this repository, {} seconds so far; this waits for as long as the service holds the index.\n",
waited.as_secs()
);
}
fn age_of(lock: &Path) -> Duration {
return std::fs::metadata(lock)
.and_then(|metadata| { return metadata.modified(); })
.ok()
.and_then(|made| { return std::time::SystemTime::now().duration_since(made).ok(); })
.unwrap_or(Duration::ZERO);
}
fn stale_line(command: &str, age: Duration) -> String {
return format!(
"cli-git: the commit service's hold on this repository's index is {} seconds old and nothing shows the service still runs, so git {command} does not wait for it.\n",
age.as_secs()
);
}
fn gone_line(command: &str) -> String {
return format!(
"cli-git: the commit service that held this repository's index no longer runs, so git {command} does not wait for it. The next commit made through the service removes the lock it left.\n"
);
}
fn gave_up_line(command: &str, waited: Duration) -> String {
return format!(
"cli-git: the commit service was still finishing a commit in this repository after {} seconds, so git {command} runs now and reports what Git says.\n",
waited.as_secs()
);
}
pub fn wait_for_service_hold(real_index: &Path, command: &str, cadence: Cadence, out: &mut dyn Write) -> Waited {
return wait_for_service_hold_within(real_index, command, SERVICE_HOLD_CEILING, cadence, out);
}
pub fn wait_for_service_hold_within(real_index: &Path, command: &str, ceiling: Duration, cadence: Cadence, out: &mut dyn Write) -> Waited {
return wait_with_watches(real_index, command, ceiling, cadence, out, &Watch::on);
}
fn wait_with_watches(real_index: &Path, command: &str, ceiling: Duration, cadence: Cadence, out: &mut dyn Write, set_watch: &dyn Fn(&Path) -> Option<Watch>) -> Waited {
let lock = lock_path_of(real_index);
let mut watch: Option<Watch> = lock.parent().and_then(set_watch);
let lock_name: std::ffi::OsString = lock.file_name().map(std::ffi::OsStr::to_os_string).unwrap_or_default();
let began: Instant = Instant::now();
let mut ticker: Ticker = Ticker::new(cadence);
let mut held: bool = false;
loop {
if holder_of(&lock) != Holder::Service {
return if held { Waited::Released } else { Waited::NotHeld };
}
if mark_names_a_gone_holder(&lock) {
let _noted = out.write_all(gone_line(command).as_bytes()).and_then(|()| { return out.flush(); });
return Waited::Stale;
}
let age: Duration = age_of(&lock);
let running: bool = marker_of(&lock).is_some_and(|marked| { return holder_is_running(marked.holder.as_ref()); });
if age >= UNASKABLE_HOLD_AGE && !running {
let _noted = out.write_all(stale_line(command, age).as_bytes()).and_then(|()| { return out.flush(); });
return Waited::Stale;
}
held = true;
let elapsed: Duration = began.elapsed();
if elapsed >= ceiling {
let _noted = out.write_all(gave_up_line(command, elapsed).as_bytes()).and_then(|()| { return out.flush(); });
return Waited::GaveUp;
}
if ticker.due(elapsed) {
let _noted = out.write_all(waiting_line(command, elapsed).as_bytes()).and_then(|()| { return out.flush(); });
}
let pause: Duration = ticker.next_in(elapsed).min(ceiling.saturating_sub(elapsed)).max(Duration::from_millis(1));
match watch.as_ref() {
Some(active) => {
if !active.wait(pause, lock_name.as_os_str()) {
drop(watch.take());
watch = lock.parent().and_then(set_watch);
}
}
None => {
std::thread::sleep(pause.min(POLL_PAUSE));
watch = lock.parent().and_then(set_watch);
}
}
}
}
pub fn wait_for_service_hold_here(real_index: &Path, command: &str) -> Waited {
return wait_for_service_hold(real_index, command, Cadence::from_environment(), &mut std::io::stderr());
}
#[cfg(test)]
#[path = "service_index_wait_tests.rs"]
mod tests;