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 )));
96 }
97 }
98 Err(error) => {
99 return Err(anyhow!(error))
100 .with_context(|| format!("create lock dir {}", lock_dir.display()));
101 }
102 }
103
104 let effective_label = if trimmed_label.is_empty() {
105 "unspecified"
106 } else {
107 trimmed_label
108 };
109 let owner = LockOwner {
110 pid: std::process::id(),
111 started_at: timeutil::now_utc_rfc3339()?,
112 command: command_line(),
113 label: effective_label.to_string(),
114 };
115
116 let owner_path = if is_task_label && lock_dir.exists() {
117 let counter = TASK_OWNER_COUNTER.fetch_add(1, Ordering::SeqCst);
118 lock_dir.join(format!(
119 "{}{}_{}",
120 TASK_OWNER_PREFIX,
121 std::process::id(),
122 counter
123 ))
124 } else {
125 lock_dir.join(OWNER_FILE_NAME)
126 };
127
128 if let Err(error) = write_lock_owner(&owner_path, &owner) {
129 if let Err(remove_error) = fs::remove_file(&owner_path) {
130 log::debug!(
131 "Failed to remove owner file {}: {}",
132 owner_path.display(),
133 remove_error
134 );
135 }
136 if let Err(remove_error) = fs::remove_dir(lock_dir) {
137 log::debug!(
138 "Failed to remove lock directory {}: {}",
139 lock_dir.display(),
140 remove_error
141 );
142 }
143 return Err(error);
144 }
145
146 Ok(DirLock {
147 lock_dir: lock_dir.to_path_buf(),
148 owner_path,
149 })
150}