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,
owner: String,
heartbeat: Option<tokio::task::JoinHandle<()>>,
}
impl Drop for OauthLock {
fn drop(&mut self) {
if let Some(handle) = self.heartbeat.take() {
handle.abort();
}
if owns(&self.dir, &self.owner) {
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 {
static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
let owner = format!(
"{}-{}-{}",
std::process::id(),
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0),
COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
);
let _ = std::fs::write(dir.join(OWNER_FILE), owner.as_bytes());
let beat_dir = dir.clone();
let beat_owner = owner.clone();
let heartbeat = tokio::runtime::Handle::try_current().ok().map(|_| {
tokio::spawn(async move {
loop {
tokio::time::sleep(HEARTBEAT).await;
if !touch(&beat_dir, &beat_owner) {
break;
}
}
})
});
OauthLock {
dir,
owner,
heartbeat,
}
}
const OWNER_FILE: &str = ".ai-usagebar-owner";
fn owns(dir: &Path, owner: &str) -> bool {
std::fs::read(dir.join(OWNER_FILE)).is_ok_and(|bytes| bytes == owner.as_bytes())
}
fn touch(dir: &Path, owner: &str) -> bool {
if !owns(dir, owner) {
return false;
}
let sentinel = dir.join(".ai-usagebar-heartbeat");
if std::fs::write(&sentinel, b"").is_ok() {
let _ = std::fs::remove_file(&sentinel);
}
true
}
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();
std::fs::write(dir.join(OWNER_FILE), b"owner").unwrap();
let before = std::fs::metadata(&dir).unwrap().modified().unwrap();
assert!(touch(&dir, "owner"));
let after = std::fs::metadata(&dir).unwrap().modified().unwrap();
assert!(after >= before);
assert!(!dir.join(".ai-usagebar-heartbeat").exists());
assert!(!touch(&dir, "someone-else"));
}
#[cfg(unix)]
#[tokio::test]
async fn dropping_previous_holder_preserves_replacement_lock() {
let td = TempDir::new().unwrap();
let target = target(&td);
let dir = lock_dir_for(&target);
let poll = Duration::from_millis(1);
let first = acquire_with(&target, Duration::from_secs(1), STALE, poll)
.await
.unwrap();
std::fs::File::open(&dir)
.unwrap()
.set_times(
std::fs::FileTimes::new()
.set_modified(SystemTime::now() - STALE - Duration::from_secs(1)),
)
.unwrap();
let replacement = acquire_with(&target, Duration::from_secs(1), STALE, poll)
.await
.unwrap();
assert!(dir.is_dir());
tokio::time::sleep(Duration::from_millis(2_600)).await;
drop(first);
assert!(dir.is_dir(), "old holder removed replacement holder's lock");
drop(replacement);
}
}