vtcode-core 0.147.2

Core library for VT Code - a Rust-based terminal coding agent
use anyhow::{Context, Result};
use std::fs::{self, File};
use std::path::Path;
use vtcode_commons::VtCodePaths;

use super::time::modified_age_secs;

/// Atomically attempts to acquire an installer lock file at `lock_path`.
///
/// Uses `create_new` so that racing processes cannot both "acquire" the
/// lock: only the first process to create the file succeeds, and every
/// other process observes `AlreadyExists` instead of silently truncating
/// and rewriting a file another process relies on for mutual exclusion.
///
/// If an existing lock file is older than `max_age_secs`, it is treated as
/// abandoned -- e.g. orphaned by a process that was killed (SIGKILL/OOM/power
/// loss) before its `Drop` impl could remove it -- and is best-effort removed
/// so a fresh attempt can proceed. Returns `Ok(None)` when another live
/// process currently holds the lock; the caller should treat this as
/// "installation already in progress" rather than an error.
pub(crate) fn acquire_lock_file(lock_path: &Path, max_age_secs: u64) -> Result<Option<File>> {
    if let Some(parent) = lock_path.parent() {
        VtCodePaths::ensure_user_dir(parent).with_context(|| format!("Failed to create {}", parent.display()))?;
    }

    if let Some(file) = try_create_new_lock_file(lock_path)? {
        return Ok(Some(file));
    }

    // Another lock file already exists. If it looks abandoned, reclaim it.
    // This retry is still race-safe: ownership is decided by whichever
    // process wins the atomic `create_new` call below, not by this
    // staleness check, so a concurrent reclaim attempt cannot corrupt state.
    if !lock_is_active(lock_path, max_age_secs) {
        let _ = fs::remove_file(lock_path);
        return try_create_new_lock_file(lock_path);
    }

    Ok(None)
}

fn try_create_new_lock_file(lock_path: &Path) -> Result<Option<File>> {
    match VtCodePaths::create_private_file(lock_path) {
        Ok(file) => Ok(Some(file)),
        Err(err)
            if err
                .downcast_ref::<std::io::Error>()
                .is_some_and(|error| error.kind() == std::io::ErrorKind::AlreadyExists) =>
        {
            Ok(None)
        }
        Err(err) => Err(err).with_context(|| format!("Failed to create lock file {}", lock_path.display())),
    }
}

pub(crate) fn lock_is_active(lock_path: &Path, max_age_secs: u64) -> bool {
    modified_age_secs(lock_path).is_some_and(|age| age < max_age_secs)
}