greentic_deployer/environment/
file_lock.rs1use std::fs::{File, OpenOptions};
14use std::path::{Path, PathBuf};
15
16use fs4::fs_std::FileExt;
17use thiserror::Error;
18
19#[derive(Debug, Error)]
20pub enum LockError {
21 #[error("could not open or create lock file at {path}: {source}")]
22 Open {
23 path: PathBuf,
24 #[source]
25 source: std::io::Error,
26 },
27 #[error("could not acquire exclusive lock at {path}: {source}")]
28 Acquire {
29 path: PathBuf,
30 #[source]
31 source: std::io::Error,
32 },
33}
34
35#[derive(Debug)]
38pub struct EnvFlock {
39 _file: File,
40}
41
42impl EnvFlock {
43 pub fn acquire(lock_path: &Path) -> Result<Self, LockError> {
45 let file = open_lock_file(lock_path)?;
46 file.lock_exclusive().map_err(|source| LockError::Acquire {
47 path: lock_path.to_path_buf(),
48 source,
49 })?;
50 Ok(Self { _file: file })
51 }
52
53 pub fn try_acquire(lock_path: &Path) -> Result<Option<Self>, LockError> {
55 let file = open_lock_file(lock_path)?;
56 match file.try_lock_exclusive() {
57 Ok(true) => Ok(Some(Self { _file: file })),
58 Ok(false) => Ok(None),
59 Err(source) => Err(LockError::Acquire {
60 path: lock_path.to_path_buf(),
61 source,
62 }),
63 }
64 }
65}
66
67fn open_lock_file(lock_path: &Path) -> Result<File, LockError> {
68 if let Some(parent) = lock_path.parent() {
69 std::fs::create_dir_all(parent).map_err(|source| LockError::Open {
70 path: parent.to_path_buf(),
71 source,
72 })?;
73 }
74 OpenOptions::new()
75 .read(true)
76 .write(true)
77 .create(true)
78 .truncate(false)
79 .open(lock_path)
80 .map_err(|source| LockError::Open {
81 path: lock_path.to_path_buf(),
82 source,
83 })
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89 use tempfile::TempDir;
90
91 #[test]
92 fn acquire_and_release() {
93 let tmp = TempDir::new().unwrap();
94 let lock_path = tmp.path().join(".lock");
95 {
96 let _guard = EnvFlock::acquire(&lock_path).unwrap();
97 assert!(lock_path.exists());
98 }
99 let _again = EnvFlock::acquire(&lock_path).unwrap();
101 }
102
103 #[test]
104 fn try_acquire_returns_none_when_held() {
105 let tmp = TempDir::new().unwrap();
106 let lock_path = tmp.path().join(".lock");
107 let _held = EnvFlock::acquire(&lock_path).unwrap();
108 let attempt = EnvFlock::try_acquire(&lock_path).unwrap();
109 assert!(attempt.is_none(), "expected try_acquire to fail while held");
110 }
111
112 #[test]
113 fn try_acquire_succeeds_after_drop() {
114 let tmp = TempDir::new().unwrap();
115 let lock_path = tmp.path().join(".lock");
116 {
117 let _held = EnvFlock::acquire(&lock_path).unwrap();
118 }
119 let attempt = EnvFlock::try_acquire(&lock_path).unwrap();
120 assert!(attempt.is_some());
121 }
122
123 #[test]
124 fn creates_parent_dir() {
125 let tmp = TempDir::new().unwrap();
126 let lock_path = tmp.path().join("nested/dir/.lock");
127 let _g = EnvFlock::acquire(&lock_path).unwrap();
128 assert!(lock_path.exists());
129 }
130}