Skip to main content

recast_core/
lockfile.rs

1//! Advisory workspace lock guarding concurrent `--apply` invocations
2//! against the same tree.
3//!
4//! Two `recast --apply` processes touching overlapping paths would
5//! interleave their rename / backup steps unpredictably. The lock
6//! is purely advisory (other tools won't see it), but every recast
7//! `--apply` checks it, so the common case (two agents on the same
8//! repo) is caught immediately with a clear error instead of leaving
9//! the tree in a partial state.
10
11use std::fs::{File, OpenOptions};
12use std::path::{Path, PathBuf};
13
14use fs2::FileExt;
15
16use crate::error::{Error, IoCtx, Result};
17
18/// RAII guard around an exclusively-locked lockfile. Drop to release.
19#[derive(Debug)]
20#[must_use = "lock is released as soon as the guard is dropped"]
21pub struct WorkspaceLock {
22    file: File,
23    path: PathBuf,
24}
25
26impl WorkspaceLock {
27    /// Path to the lockfile this guard is holding.
28    pub fn path(&self) -> &Path {
29        &self.path
30    }
31}
32
33impl Drop for WorkspaceLock {
34    fn drop(&mut self) {
35        let _ = self.file.unlock();
36    }
37}
38
39/// Try to take an exclusive non-blocking lock on `lock_path`. Returns
40/// [`Error::Locked`] immediately if another process already holds it.
41pub fn acquire_workspace_lock(lock_path: &Path) -> Result<WorkspaceLock> {
42    if let Some(parent) = lock_path.parent() {
43        std::fs::create_dir_all(parent).io_ctx(lock_path)?;
44    }
45    let file = OpenOptions::new()
46        .read(true)
47        .write(true)
48        .create(true)
49        .truncate(false)
50        .open(lock_path)
51        .io_ctx(lock_path)?;
52
53    // try_lock_exclusive surfaces WouldBlock when the lock is held by
54    // another process; every other io::Error (EPERM, ENOSPC, EIO, …)
55    // is a real failure that should propagate as Error::Io instead of
56    // being misclassified as "another recast is already applying".
57    if let Err(e) = file.try_lock_exclusive() {
58        return match e.kind() {
59            std::io::ErrorKind::WouldBlock => Err(Error::Locked { path: lock_path.to_path_buf() }),
60            _ => Err(Error::Io { path: lock_path.to_path_buf(), source: e }),
61        };
62    }
63    Ok(WorkspaceLock { file, path: lock_path.to_path_buf() })
64}
65
66#[cfg(test)]
67#[path = "lockfile_tests.rs"]
68mod tests;