use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use crate::error::{AppError, Result};
pub const STALE: Duration = Duration::from_secs(5);
const HEARTBEAT: Duration = Duration::from_millis(2_500);
const POLL: Duration = Duration::from_millis(250);
pub const ACQUIRE_TIMEOUT: Duration = Duration::from_secs(20);
pub fn lock_dir_for(target: &Path) -> PathBuf {
let mut dir = target.as_os_str().to_os_string();
dir.push(".lock");
PathBuf::from(dir)
}
#[derive(Debug)]
pub struct OauthLock {
dir: PathBuf,
heartbeat: Option<tokio::task::JoinHandle<()>>,
}
impl Drop for OauthLock {
fn drop(&mut self) {
if let Some(handle) = self.heartbeat.take() {
handle.abort();
}
let _ = std::fs::remove_dir_all(&self.dir);
}
}
pub async fn acquire(target: &Path) -> Result<OauthLock> {
acquire_with(target, ACQUIRE_TIMEOUT, STALE, POLL).await
}
pub async fn acquire_with(
target: &Path,
timeout: Duration,
stale: Duration,
poll: Duration,
) -> Result<OauthLock> {
let dir = lock_dir_for(target);
if let Some(parent) = dir.parent() {
std::fs::create_dir_all(parent).map_err(|e| AppError::io_at(parent, e))?;
}
let deadline = tokio::time::Instant::now() + timeout;
loop {
match std::fs::create_dir(&dir) {
Ok(()) => return Ok(hold(dir)),
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
if is_stale(&dir, stale) {
let _ = std::fs::remove_dir_all(&dir);
}
}
Err(e) => return Err(AppError::io_at(&dir, e)),
}
if tokio::time::Instant::now() >= deadline {
return Err(AppError::Transport(format!(
"timed out waiting for the Kimi Code CLI credential lock at {}",
crate::display::sanitize_untrusted_path(&dir)
)));
}
tokio::time::sleep(poll).await;
}
}
fn hold(dir: PathBuf) -> OauthLock {
let beat_dir = dir.clone();
let heartbeat = tokio::runtime::Handle::try_current().ok().map(|_| {
tokio::spawn(async move {
loop {
tokio::time::sleep(HEARTBEAT).await;
touch(&beat_dir);
}
})
});
OauthLock { dir, heartbeat }
}
fn touch(dir: &Path) {
let sentinel = dir.join(".ai-usagebar-heartbeat");
if std::fs::write(&sentinel, b"").is_ok() {
let _ = std::fs::remove_file(&sentinel);
}
}
fn is_stale(dir: &Path, stale: Duration) -> bool {
let Ok(modified) = std::fs::metadata(dir).and_then(|m| m.modified()) else {
return false;
};
SystemTime::now()
.duration_since(modified)
.is_ok_and(|age| age > stale)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn target(td: &TempDir) -> PathBuf {
td.path().join("oauth").join("kimi-code")
}
#[test]
fn lock_dir_matches_proper_lockfile_naming() {
assert_eq!(
lock_dir_for(Path::new("/home/u/.kimi-code/oauth/kimi-code")),
PathBuf::from("/home/u/.kimi-code/oauth/kimi-code.lock")
);
}
#[tokio::test]
async fn acquire_creates_the_lock_directory_and_drop_removes_it() {
let td = TempDir::new().unwrap();
let target = target(&td);
let dir = lock_dir_for(&target);
{
let _lock = acquire_with(&target, Duration::from_secs(1), STALE, POLL)
.await
.unwrap();
assert!(dir.is_dir());
}
assert!(!dir.exists());
}
#[tokio::test]
async fn a_fresh_foreign_lock_is_waited_out_not_stolen() {
let td = TempDir::new().unwrap();
let target = target(&td);
let dir = lock_dir_for(&target);
std::fs::create_dir_all(&dir).unwrap();
let err = acquire_with(
&target,
Duration::from_millis(60),
Duration::from_secs(3_600),
Duration::from_millis(10),
)
.await
.unwrap_err();
assert!(matches!(err, AppError::Transport(_)), "{err:?}");
assert!(dir.is_dir(), "a live peer's lock must survive");
}
#[tokio::test]
async fn an_abandoned_lock_is_stolen_once_stale() {
let td = TempDir::new().unwrap();
let target = target(&td);
let dir = lock_dir_for(&target);
std::fs::create_dir_all(&dir).unwrap();
let lock = acquire_with(
&target,
Duration::from_secs(2),
Duration::ZERO,
Duration::from_millis(10),
)
.await
.unwrap();
assert!(dir.is_dir());
drop(lock);
assert!(!dir.exists());
}
#[tokio::test]
async fn a_leftover_heartbeat_sentinel_does_not_block_release() {
let td = TempDir::new().unwrap();
let target = target(&td);
let dir = lock_dir_for(&target);
{
let _lock = acquire_with(&target, Duration::from_secs(1), STALE, POLL)
.await
.unwrap();
std::fs::write(dir.join(".ai-usagebar-heartbeat"), b"").unwrap();
}
assert!(!dir.exists());
}
#[test]
fn touch_moves_the_directory_mtime_forward() {
let td = TempDir::new().unwrap();
let dir = td.path().join("kimi-code.lock");
std::fs::create_dir(&dir).unwrap();
let before = std::fs::metadata(&dir).unwrap().modified().unwrap();
touch(&dir);
let after = std::fs::metadata(&dir).unwrap().modified().unwrap();
assert!(after >= before);
assert!(!dir.join(".ai-usagebar-heartbeat").exists());
}
}