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;