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