ralph/lock/
acquisition.rs1use super::{
17 DirLock,
18 owner::{
19 LockOwner, OWNER_FILE_NAME, TASK_OWNER_PREFIX, command_line, is_supervising_label,
20 parse_lock_owner, read_lock_owner, write_lock_owner,
21 },
22 stale::{format_lock_error, inspect_existing_lock},
23};
24use crate::timeutil;
25use anyhow::{Context, Result, anyhow};
26use std::fs;
27use std::path::{Path, PathBuf};
28use std::sync::atomic::{AtomicUsize, Ordering};
29
30static TASK_OWNER_COUNTER: AtomicUsize = AtomicUsize::new(0);
31
32pub fn queue_lock_dir(repo_root: &Path) -> PathBuf {
33 repo_root.join(".ralph").join("lock")
34}
35
36pub fn is_supervising_process(lock_dir: &Path) -> Result<bool> {
37 let owner_path = lock_dir.join(OWNER_FILE_NAME);
38 let raw = match fs::read_to_string(&owner_path) {
39 Ok(raw) => raw,
40 Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(false),
41 Err(err) => {
42 return Err(anyhow!(err))
43 .with_context(|| format!("read lock owner {}", owner_path.display()));
44 }
45 };
46
47 let owner = match parse_lock_owner(&raw) {
48 Some(owner) => owner,
49 None => return Ok(false),
50 };
51 Ok(is_supervising_label(&owner.label))
52}
53
54pub fn acquire_dir_lock(lock_dir: &Path, label: &str, force: bool) -> Result<DirLock> {
55 log::debug!(
56 "acquiring dir lock: {} (label: {})",
57 lock_dir.display(),
58 label
59 );
60 if let Some(parent) = lock_dir.parent() {
61 fs::create_dir_all(parent)
62 .with_context(|| format!("create lock parent {}", parent.display()))?;
63 }
64
65 let trimmed_label = label.trim();
66 let is_task_label = trimmed_label == "task";
67
68 match fs::create_dir(lock_dir) {
69 Ok(()) => {}
70 Err(error) if error.kind() == std::io::ErrorKind::AlreadyExists => {
71 let existing = inspect_existing_lock(lock_dir, read_lock_owner);
72
73 if force && existing.is_stale {
74 if let Err(remove_error) = fs::remove_dir_all(lock_dir) {
75 log::debug!(
76 "Failed to remove stale lock directory {}: {}",
77 lock_dir.display(),
78 remove_error
79 );
80 }
81 return acquire_dir_lock(lock_dir, label, false);
82 }
83
84 if !(is_task_label
85 && existing
86 .owner
87 .as_ref()
88 .is_some_and(|owner| is_supervising_label(&owner.label)))
89 {
90 return Err(anyhow!(format_lock_error(
91 lock_dir,
92 existing.owner.as_ref(),
93 existing.is_stale,
94 existing.owner_unreadable,
95 existing.staleness,
96 )));
97 }
98 }
99 Err(error) => {
100 return Err(anyhow!(error))
101 .with_context(|| format!("create lock dir {}", lock_dir.display()));
102 }
103 }
104
105 let effective_label = if trimmed_label.is_empty() {
106 "unspecified"
107 } else {
108 trimmed_label
109 };
110 let owner = LockOwner {
111 pid: std::process::id(),
112 started_at: timeutil::now_utc_rfc3339()?,
113 command: command_line(),
114 label: effective_label.to_string(),
115 };
116
117 let owner_path = if is_task_label && lock_dir.exists() {
118 let counter = TASK_OWNER_COUNTER.fetch_add(1, Ordering::SeqCst);
119 lock_dir.join(format!(
120 "{}{}_{}",
121 TASK_OWNER_PREFIX,
122 std::process::id(),
123 counter
124 ))
125 } else {
126 lock_dir.join(OWNER_FILE_NAME)
127 };
128
129 if let Err(error) = write_lock_owner(&owner_path, &owner) {
130 if let Err(remove_error) = fs::remove_file(&owner_path) {
131 log::debug!(
132 "Failed to remove owner file {}: {}",
133 owner_path.display(),
134 remove_error
135 );
136 }
137 if let Err(remove_error) = fs::remove_dir(lock_dir) {
138 log::debug!(
139 "Failed to remove lock directory {}: {}",
140 lock_dir.display(),
141 remove_error
142 );
143 }
144 return Err(error);
145 }
146
147 Ok(DirLock {
148 lock_dir: lock_dir.to_path_buf(),
149 owner_path,
150 })
151}