start_command/
store_lock.rs1use crate::local_hostname;
3use serde_json::{json, Value};
4use std::fs::{self, OpenOptions};
5use std::io::Write;
6use std::path::PathBuf;
7use std::thread;
8use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
9use uuid::Uuid;
10
11pub struct LockManager {
12 path: PathBuf,
13 token: Option<String>,
14 monitor_owner: bool,
15 metadata: serde_json::Map<String, Value>,
17}
18
19impl LockManager {
20 pub fn new(path: PathBuf) -> Self {
21 Self {
22 path,
23 token: None,
24 monitor_owner: false,
25 metadata: serde_json::Map::new(),
26 }
27 }
28
29 pub fn with_metadata(mut self, key: &str, value: Value) -> Self {
31 self.metadata.insert(key.to_string(), value);
32 self
33 }
34
35 pub fn read_lock_file(&self) -> Option<Value> {
37 fs::read_to_string(&self.path)
38 .ok()
39 .and_then(|s| serde_json::from_str::<Value>(&s).ok())
40 }
41
42 pub fn for_cpu_monitor(mut self) -> Self {
43 self.monitor_owner = true;
44 self
45 }
46
47 pub fn acquire(&mut self, timeout: u64) -> bool {
48 let started = Instant::now();
49 let debug = |message: &str| {
50 if std::env::var("START_DEBUG").as_deref() == Ok("1") {
51 eprintln!("[DEBUG] Store lock {}: {}", self.path.display(), message);
52 }
53 };
54 while started.elapsed() < Duration::from_millis(timeout) {
55 if let Ok(stat) = fs::metadata(&self.path) {
56 let data = fs::read_to_string(&self.path)
57 .ok()
58 .and_then(|s| serde_json::from_str::<Value>(&s).ok());
59 if self.stale(data.as_ref(), &stat) {
60 if let Ok(current) = fs::metadata(&self.path) {
61 if Self::same_file(&stat, ¤t) {
62 debug("reclaiming stale or malformed lock");
63 let _ = fs::remove_file(&self.path);
64 }
65 }
66 }
67 }
68 let token = Uuid::new_v4().to_string();
69 let temp = self
70 .path
71 .with_extension(format!("lock.{}.{}", std::process::id(), token));
72 let result = (|| -> std::io::Result<()> {
73 let mut file = OpenOptions::new()
74 .write(true)
75 .create_new(true)
76 .open(&temp)?;
77 let owner = json!({"pid": std::process::id(), "timestamp":
78 SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_millis(),
79 "hostname": local_hostname::get().map(|h| h.to_string_lossy().to_string()).unwrap_or_default(),
80 "token": token, "processStart": Self::process_info(std::process::id() as u64).map(|p| p.1)});
81 let mut data = self.metadata.clone();
83 if let Value::Object(owner) = owner {
84 data.extend(owner);
85 }
86 file.write_all(Value::Object(data).to_string().as_bytes())?;
87 file.sync_all()?;
88 fs::hard_link(&temp, &self.path)
89 })();
90 let _ = fs::remove_file(&temp);
91 match result {
92 Ok(()) => {
93 debug(&format!(
94 "acquired after {}ms",
95 started.elapsed().as_millis()
96 ));
97 self.token = Some(token);
98 return true;
99 }
100 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
101 thread::sleep(Duration::from_millis(100));
102 }
103 Err(error) => {
104 debug(&format!("publication failed: {}", error));
105 return false;
106 }
107 }
108 }
109 debug("acquisition timed out");
110 false
111 }
112
113 fn same_file(a: &fs::Metadata, b: &fs::Metadata) -> bool {
114 #[cfg(unix)]
115 {
116 use std::os::unix::fs::MetadataExt;
117 a.dev() == b.dev() && a.ino() == b.ino() && a.modified().ok() == b.modified().ok()
118 }
119 #[cfg(not(unix))]
120 {
121 a.modified().ok() == b.modified().ok() && a.len() == b.len()
122 }
123 }
124
125 fn stale(&self, data: Option<&Value>, stat: &fs::Metadata) -> bool {
126 let timestamp = data
127 .and_then(|d| d.get("timestamp"))
128 .and_then(Value::as_u64);
129 let pid = data.and_then(|d| d.get("pid")).and_then(Value::as_u64);
130 let (Some(timestamp), Some(pid)) =
131 (timestamp, pid.filter(|p| *p > 0 && *p <= i32::MAX as u64))
132 else {
133 return stat
134 .modified()
135 .ok()
136 .and_then(|m| m.elapsed().ok())
137 .is_some_and(|age| age >= Duration::from_secs(3));
138 };
139 let hostname = crate::launch_owner::hostname();
140 if data.and_then(|d| d.get("hostname")).and_then(Value::as_str) == Some(&hostname) {
141 if !crate::launch_owner::process_alive(pid) {
142 return true;
143 }
144 if let Some((state, start)) = Self::process_info(pid) {
145 if state == "Z"
146 || state == "X"
147 || state == "x"
148 || data
149 .and_then(|d| d["processStart"].as_str())
150 .is_some_and(|s| s != start)
151 {
152 return true;
153 }
154 }
155 #[cfg(target_os = "linux")]
156 if self.monitor_owner && data.and_then(|d| d["processStart"].as_str()).is_none() {
157 if let Ok(command) = fs::read_to_string(format!("/proc/{}/cmdline", pid)) {
158 return !command.contains("cpu-penalty-monitor")
159 && !command.contains("__start-cpu-monitor");
160 }
161 }
162 return false;
163 }
164 let now = SystemTime::now()
165 .duration_since(UNIX_EPOCH)
166 .unwrap_or_default()
167 .as_millis() as u64;
168 now.saturating_sub(timestamp) > 60000
169 }
170
171 fn process_info(pid: u64) -> Option<(String, String)> {
172 #[cfg(target_os = "linux")]
173 {
174 let stat = fs::read_to_string(format!("/proc/{}/stat", pid)).ok()?;
175 let fields: Vec<&str> = stat[stat.rfind(')')? + 2..].split_whitespace().collect();
176 Some((fields.first()?.to_string(), fields.get(19)?.to_string()))
177 }
178 #[cfg(not(target_os = "linux"))]
179 {
180 let _ = pid;
181 None
182 }
183 }
184
185 pub fn release(&mut self) {
186 if let Some(token) = self.token.take() {
187 let data = fs::read_to_string(&self.path)
188 .ok()
189 .and_then(|s| serde_json::from_str::<Value>(&s).ok());
190 if data
191 .as_ref()
192 .and_then(|d| d.get("token"))
193 .and_then(Value::as_str)
194 == Some(&token)
195 {
196 let _ = fs::remove_file(&self.path);
197 }
198 }
199 }
200}
201
202impl Drop for LockManager {
203 fn drop(&mut self) {
204 self.release();
205 }
206}