use crate::service::Permit;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex, PoisonError};
use std::thread;
use std::time::{Duration, SystemTime};
#[derive(Debug, Clone, Copy)]
pub(crate) struct Timing {
stale: Duration,
heartbeat: Duration,
retries: u32,
backoff: Duration,
most: Duration,
}
pub(crate) const WRITE: Timing = Timing {
stale: Duration::from_millis(15_000),
heartbeat: Duration::from_millis(7_500),
retries: 10,
backoff: Duration::from_millis(100),
most: Duration::from_millis(1_000),
};
pub(crate) const REFRESH: Timing = Timing {
stale: Duration::from_millis(60_000),
heartbeat: Duration::from_millis(5_000),
..WRITE
};
impl Timing {
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn asked(self, retries: u32, every: Duration) -> Timing {
Timing {
retries,
backoff: every,
most: every,
..self
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Locks {
pub(crate) write: Timing,
pub(crate) refresh: Timing,
}
impl Locks {
pub(crate) const CLAUDE_CODE: Locks = Locks {
write: WRITE,
refresh: REFRESH,
};
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum LockError {
#[error(
"another process is writing or renewing credentials right now; try again in a few seconds"
)]
Busy,
#[error("cannot take the credential lock: {0}")]
Io(#[source] io::Error),
}
type Stop = Arc<(Mutex<bool>, Condvar)>;
impl LockError {
pub fn code(&self) -> &'static str {
match self {
LockError::Busy => "switch_in_progress",
LockError::Io(_) => "lock_unavailable",
}
}
}
pub(crate) struct Guard {
path: PathBuf,
permit: Permit,
stop: Stop,
beat: Option<thread::JoinHandle<()>>,
compromised: Arc<AtomicBool>,
}
impl Guard {
pub(crate) fn compromised(&self) -> bool {
self.compromised.load(Ordering::Acquire)
}
}
impl Drop for Guard {
fn drop(&mut self) {
let (flag, wake) = &*self.stop;
*flag.lock().unwrap_or_else(PoisonError::into_inner) = true;
wake.notify_all();
if let Some(handle) = self.beat.take() {
let _ = handle.join();
}
if !self.compromised() {
let _ = crate::host::fs::remove_dir(self.permit, &self.path);
}
}
}
fn age(path: &Path) -> Option<Duration> {
let mtime = std::fs::metadata(path).ok()?.modified().ok()?;
SystemTime::now().duration_since(mtime).ok()
}
fn mtime(path: &Path) -> io::Result<SystemTime> {
std::fs::metadata(path)?.modified()
}
fn touch(permit: Permit, path: &Path, at: SystemTime) -> io::Result<SystemTime> {
crate::host::fs::touch_dir(permit, path, at)?;
mtime(path)
}
pub(crate) fn acquire(permit: Permit, target: &Path, timing: Timing) -> Result<Guard, LockError> {
let path = PathBuf::from(format!("{}.lock", target.display()));
if let Some(parent) = path.parent() {
crate::host::fs::create_dir_all(permit, parent).map_err(LockError::Io)?;
}
let mut backoff = timing.backoff;
for attempt in 0..=timing.retries {
match crate::host::fs::create_dir(permit, &path) {
Ok(()) => return start(permit, path, timing).map_err(LockError::Io),
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => {
if abandoned(&path, timing) {
let _ = crate::host::fs::remove_dir(permit, &path);
continue;
}
}
Err(e) => return Err(LockError::Io(e)),
}
if attempt < timing.retries {
thread::sleep(backoff);
backoff = (backoff * 2).min(timing.most);
}
}
Err(LockError::Busy)
}
fn abandoned(path: &Path, timing: Timing) -> bool {
age(path).is_some_and(|a| a > timing.stale)
}
fn start(permit: Permit, path: PathBuf, timing: Timing) -> io::Result<Guard> {
let mut held = mtime(&path)?;
let stop: Stop = Arc::new((Mutex::new(false), Condvar::new()));
let compromised = Arc::new(AtomicBool::new(false));
let beat = {
let (path, stop, compromised) = (path.clone(), Arc::clone(&stop), Arc::clone(&compromised));
thread::spawn(move || {
let (flag, wake) = &*stop;
let mut stopped = flag.lock().unwrap_or_else(PoisonError::into_inner);
loop {
let (next, _) = wake
.wait_timeout_while(stopped, timing.heartbeat, |stop| !*stop)
.unwrap_or_else(PoisonError::into_inner);
stopped = next;
if *stopped {
return;
}
match mtime(&path) {
Ok(found) if found == held => {}
_ => {
compromised.store(true, Ordering::Release);
return;
}
}
match touch(permit, &path, SystemTime::now()) {
Ok(stored) => held = stored,
Err(_) => {
compromised.store(true, Ordering::Release);
return;
}
}
}
})
};
Ok(Guard {
path,
permit,
stop,
beat: Some(beat),
compromised,
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::mpsc;
use std::time::Instant;
const PATIENCE: Duration = Duration::from_secs(20);
const QUICK: Timing = Timing {
heartbeat: Duration::from_millis(1),
retries: 2,
backoff: Duration::from_millis(1),
most: Duration::from_millis(1),
..WRITE
};
fn heartbeat_ends(guard: &mut Guard) {
let beat = guard.beat.take().expect("a heartbeat");
let (ended, ends) = mpsc::channel();
thread::spawn(move || {
let _ = beat.join();
let _ = ended.send(());
});
ends.recv_timeout(PATIENCE).expect("the heartbeat ended");
}
fn until(what: &str, done: impl Fn() -> bool) {
let started = Instant::now();
while !done() {
assert!(started.elapsed() < PATIENCE, "never {what}");
thread::sleep(Duration::from_millis(1));
}
}
fn age_past_staleness(lock: &Path) {
touch(
Permit::for_a_test(),
lock,
SystemTime::now() - Duration::from_secs(3600),
)
.unwrap();
}
fn scratch() -> (tempfile::TempDir, PathBuf) {
let dir = tempfile::tempdir().unwrap();
let target = dir.path().join("target");
(dir, target)
}
#[test]
#[cfg_attr(windows, ignore = "W16: Pitboard's locks on Windows")]
fn acquires_and_releases() {
let (_dir, t) = scratch();
let lock = t.with_extension("").parent().unwrap().join("target.lock");
{
let _g = acquire(Permit::for_a_test(), &t, WRITE).expect("should acquire");
assert!(lock.is_dir(), "the lock directory should exist while held");
}
assert!(
!lock.exists(),
"dropping the guard must remove the directory"
);
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn refuses_while_another_holder_is_alive() {
let (_dir, t) = scratch();
let _held = acquire(Permit::for_a_test(), &t, QUICK).expect("first acquire");
assert!(matches!(
acquire(Permit::for_a_test(), &t, QUICK),
Err(LockError::Busy)
));
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn reclaims_a_lock_left_behind_by_a_dead_process() {
let (_dir, t) = scratch();
let lock = PathBuf::from(format!("{}.lock", t.display()));
std::fs::create_dir_all(&lock).unwrap();
age_past_staleness(&lock);
let _g =
acquire(Permit::for_a_test(), &t, WRITE).expect("a stale lock must be reclaimable");
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn a_refresh_lock_is_abandoned_only_once_claude_code_would_take_it() {
let (_dir, t) = scratch();
let lock = t.with_extension("lock");
std::fs::create_dir_all(&lock).unwrap();
touch(
Permit::for_a_test(),
&lock,
SystemTime::now() - Duration::from_secs(20),
)
.unwrap();
assert!(abandoned(&lock, WRITE));
assert!(!abandoned(&lock, REFRESH));
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn releasing_is_immediate() {
let (_dir, t) = scratch();
let hourly = Timing {
heartbeat: Duration::from_secs(3600),
..WRITE
};
let held = acquire(Permit::for_a_test(), &t, hourly).unwrap();
let (released, done) = mpsc::channel();
thread::spawn(move || {
drop(held);
let _ = released.send(());
});
done.recv_timeout(PATIENCE)
.expect("released without waiting out the heartbeat");
assert!(!PathBuf::from(format!("{}.lock", t.display())).exists());
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn the_heartbeat_keeps_a_held_lock_young() {
let (_dir, t) = scratch();
let g = acquire(Permit::for_a_test(), &t, QUICK).unwrap();
let lock = PathBuf::from(format!("{}.lock", t.display()));
for _ in 0..2 {
let touched = mtime(&lock).unwrap();
until("touched by the heartbeat", || {
mtime(&lock).unwrap() != touched
});
}
assert!(age(&lock).unwrap() < QUICK.stale);
assert!(!g.compromised(), "nobody else touched it");
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn a_lock_somebody_else_touched_is_never_ours_again() {
let (_dir, t) = scratch();
let mut g = acquire(Permit::for_a_test(), &t, QUICK).unwrap();
let lock = PathBuf::from(format!("{}.lock", t.display()));
{
let _between_beats = g.stop.0.lock().unwrap();
age_past_staleness(&lock);
}
heartbeat_ends(&mut g);
assert!(
g.compromised(),
"a lock whose mtime this guard did not set is not this guard's"
);
drop(g);
assert!(
lock.exists(),
"and releasing it must not take away the lock that now belongs to somebody else"
);
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn a_lock_that_vanished_is_not_quietly_remade() {
let (_dir, t) = scratch();
let mut g = acquire(Permit::for_a_test(), &t, QUICK).unwrap();
let lock = PathBuf::from(format!("{}.lock", t.display()));
std::fs::remove_dir(&lock).unwrap();
heartbeat_ends(&mut g);
assert!(g.compromised());
assert!(!lock.exists(), "the heartbeat did not put it back");
}
#[test]
#[cfg_attr(
windows,
ignore = "W16: Pitboard writing, replacing and removing files on Windows"
)]
fn what_the_filesystem_stored_is_what_gets_remembered() {
let (_dir, t) = scratch();
let _g = acquire(Permit::for_a_test(), &t, WRITE).unwrap();
let lock = PathBuf::from(format!("{}.lock", t.display()));
let asked = SystemTime::now();
let stored = touch(Permit::for_a_test(), &lock, asked).unwrap();
assert_eq!(
stored,
mtime(&lock).unwrap(),
"reading it back twice gives the same answer, whatever it is"
);
}
}