git_policy_cli/
service_index_lock.rs1use super::index_lock_evidence::{Verdict, classify_index_lock, gather_index_lock_evidence, proven_holder_line, proven_holder_progress_line};
20use super::service_protocol::FailureCode;
21use std::io::Write;
22use std::path::{Path, PathBuf};
23use std::time::{Duration, Instant};
24
25pub const LOCK_MARKER: &str = "cli-git-service ";
27
28pub const NOTICE_EVERY: Duration = Duration::from_secs(10);
30
31pub const NOTICE_VARIABLE: &str = "CLI_GIT_WAIT_NOTICE_SECONDS";
33
34fn notice_every() -> Duration {
36 return std::env::var(NOTICE_VARIABLE).ok().and_then(|text| { return text.trim().parse::<u64>().ok(); }).filter(|seconds| { return *seconds > 0; }).map_or(NOTICE_EVERY, Duration::from_secs);
37}
38
39#[derive(Clone, Debug, Eq, PartialEq)]
41pub enum Announce {
42 Waiting(String),
44 Holder(String),
46}
47
48#[derive(Clone, Copy, Debug, Eq, PartialEq)]
50pub struct LockFailure {
51 pub code: FailureCode,
53}
54
55#[derive(Debug)]
57pub struct IndexLock {
58 lock: PathBuf,
59 index: PathBuf,
60 armed: bool,
61}
62
63pub struct Watch {
65 #[cfg(target_os = "linux")]
66 descriptor: i32,
67}
68
69#[cfg(target_os = "linux")]
70impl Watch {
71 pub fn on(directory: &Path) -> Option<Watch> {
72 use std::os::unix::ffi::OsStrExt;
73 let descriptor: i32 = unsafe { libc::inotify_init1(libc::IN_CLOEXEC) };
75 if descriptor < 0 {
76 return None;
77 }
78 let path = std::ffi::CString::new(directory.as_os_str().as_bytes()).ok()?;
79 let added: i32 = unsafe { libc::inotify_add_watch(descriptor, path.as_ptr(), libc::IN_DELETE | libc::IN_MOVED_FROM) };
81 if added < 0 {
82 unsafe { libc::close(descriptor) };
84 return None;
85 }
86 return Some(Watch { descriptor });
87 }
88
89 pub fn wait(&self, timeout: Duration) {
91 let mut entry = libc::pollfd { fd: self.descriptor, events: libc::POLLIN, revents: 0 };
92 let milliseconds: i32 = i32::try_from(timeout.as_millis()).unwrap_or(i32::MAX);
93 let ready: i32 = unsafe { libc::poll(&raw mut entry, 1, milliseconds) };
95 if ready > 0 {
96 let mut buffer: [u8; 4096] = [0; 4096];
97 let _read = unsafe { libc::read(self.descriptor, buffer.as_mut_ptr().cast(), buffer.len()) };
99 }
100 }
101}
102
103#[cfg(target_os = "linux")]
104impl Drop for Watch {
105 fn drop(&mut self) {
106 unsafe { libc::close(self.descriptor) };
108 }
109}
110
111#[cfg(not(target_os = "linux"))]
112impl Watch {
113 pub fn on(_directory: &Path) -> Option<Watch> {
114 return None;
115 }
116
117 pub fn wait(&self, _timeout: Duration) {}
118}
119
120pub fn try_acquire(index: &Path, seq: u64) -> Option<IndexLock> {
131 let lock: PathBuf = lock_path_of(index);
132 let mut file = match std::fs::OpenOptions::new().write(true).create_new(true).open(&lock) {
133 Ok(file) => file,
134 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists && is_services_own(&lock) => {
135 let _removed = std::fs::remove_file(&lock);
136 std::fs::OpenOptions::new().write(true).create_new(true).open(&lock).ok()?
137 }
138 Err(_) => return None,
139 };
140 let _marked = writeln!(file, "{LOCK_MARKER}{seq}");
141 return Some(IndexLock { lock, index: index.to_path_buf(), armed: true });
142}
143
144fn is_services_own(lock: &Path) -> bool {
146 let Ok(mut file) = std::fs::File::open(lock) else { return false };
147 let mut head: [u8; LOCK_MARKER.len()] = [0; LOCK_MARKER.len()];
148 return std::io::Read::read_exact(&mut file, &mut head).is_ok() && head == *LOCK_MARKER.as_bytes();
149}
150
151pub fn lock_path_of(index: &Path) -> PathBuf {
153 let mut name = index.as_os_str().to_os_string();
154 name.push(".lock");
155 return PathBuf::from(name);
156}
157
158fn holder_words(pid: i64, command: Option<&str>) -> String {
160 return match command {
161 Some(name) => format!("Process {pid} ({name})"),
162 None => format!("Process {pid}"),
163 };
164}
165
166fn holder_notice(index: &Path, waited: Duration, limit: Duration, first: bool) -> (Option<String>, Option<String>) {
168 let Ok(Some(evidence)) = gather_index_lock_evidence(index) else { return (None, None) };
169 let Verdict::ProvenAlive { pid, by_descriptor, command } = classify_index_lock(&evidence) else {
170 let line: String = format!(
171 "cli-git: waiting for {}, whose owner cannot be proven alive, after {} seconds; giving up after {} seconds.\n",
172 evidence.lock_path.display(),
173 waited.as_secs(),
174 limit.as_secs()
175 );
176 return (Some(line), None);
177 };
178 let line: String = if first {
179 proven_holder_line(pid, by_descriptor, command.as_deref(), evidence.lock_path.as_path(), limit.as_secs())
180 } else {
181 proven_holder_progress_line(pid, by_descriptor, command.as_deref(), evidence.lock_path.as_path(), waited.as_secs(), limit.as_secs())
182 };
183 return (Some(line), Some(holder_words(pid, command.as_deref())));
184}
185
186pub fn acquire(index: &Path, seq: u64, limit: Duration, announce: &dyn Fn(Announce)) -> Result<IndexLock, LockFailure> {
203 let lock: PathBuf = lock_path_of(index);
204 let directory: &Path = lock.parent().unwrap_or(Path::new("."));
205 let watch: Option<Watch> = Watch::on(directory);
206 let began: Instant = Instant::now();
207 let mut first: bool = true;
208 let mut last_notice: Instant = began;
209 loop {
210 match std::fs::OpenOptions::new().write(true).create_new(true).open(&lock) {
211 Ok(mut file) => {
212 let _marked = writeln!(file, "{LOCK_MARKER}{seq}");
213 return Ok(IndexLock { lock, index: index.to_path_buf(), armed: true });
214 }
215 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
216 if is_services_own(&lock) {
219 let _removed = std::fs::remove_file(&lock);
220 continue;
221 }
222 }
223 Err(error) => return Err(LockFailure { code: super::service_land_follow::storage_failure(&error) }),
224 }
225 let waited: Duration = began.elapsed();
226 let exhausted: bool = waited >= limit || watch.is_none();
227 let due: bool = first || last_notice.elapsed() >= notice_every();
229 let (line, who) = if due || exhausted { holder_notice(index, waited, limit, first) } else { (None, None) };
230 if due {
231 if let Some(text) = line {
232 announce(Announce::Waiting(text));
233 }
234 first = false;
235 last_notice = Instant::now();
236 }
237 if exhausted {
238 announce(Announce::Holder(who.unwrap_or_else(|| { return String::from("Another program"); })));
239 return Err(LockFailure { code: FailureCode::IndexInUse });
240 }
241 if let Some(active) = watch.as_ref() {
242 active.wait(notice_every().saturating_sub(last_notice.elapsed()).max(Duration::from_millis(1)).min(limit.saturating_sub(waited)));
243 }
244 }
245}
246
247impl IndexLock {
248 pub fn release_with(mut self, built: &Path) -> Result<(), std::io::Error> {
262 if built.exists() {
263 std::fs::copy(built, &self.lock)?;
264 std::fs::rename(&self.lock, &self.index)?;
265 let _removed = std::fs::remove_file(built);
266 } else {
267 std::fs::remove_file(&self.lock)?;
269 }
270 self.armed = false;
271 return Ok(());
272 }
273}
274
275impl Drop for IndexLock {
276 fn drop(&mut self) {
277 if self.armed {
278 let _removed = std::fs::remove_file(&self.lock);
279 }
280 }
281}
282
283pub fn remove_stale(index: &Path, seq: u64) {
293 let lock: PathBuf = lock_path_of(index);
294 if std::fs::read_to_string(&lock).is_ok_and(|text| { return text == format!("{LOCK_MARKER}{seq}\n"); }) {
295 let _removed = std::fs::remove_file(&lock);
296 }
297}
298
299#[cfg(test)]
301#[path = "service_index_lock_tests.rs"]
302mod tests;