1use crate::{hash_path, user_forc_directory};
2use std::{
3 fs::{create_dir_all, read_dir, remove_file, File},
4 io::{self, Read, Write},
5 path::{Path, PathBuf},
6};
7
8pub struct PidFileLocking(PathBuf);
15
16impl PidFileLocking {
17 pub fn new<X: AsRef<Path>, Y: AsRef<Path>>(
18 filename: X,
19 dir: Y,
20 extension: &str,
21 ) -> PidFileLocking {
22 let _ = Self::cleanup_stale_files();
24
25 let file_name = hash_path(filename);
26 Self(
27 user_forc_directory()
28 .join(dir)
29 .join(file_name)
30 .with_extension(extension),
31 )
32 }
33
34 pub fn lsp<X: AsRef<Path>>(filename: X) -> PidFileLocking {
37 Self::new(filename, ".lsp-locks", "lock")
38 }
39
40 #[cfg(not(target_os = "windows"))]
42 fn is_pid_active(pid: usize) -> bool {
43 use std::process::Command;
46 let output = Command::new("ps")
47 .arg("-p")
48 .arg(pid.to_string())
49 .output()
50 .expect("Failed to execute ps command");
51
52 let output_str = String::from_utf8_lossy(&output.stdout);
53 output_str.contains(&format!("{} ", pid))
54 }
55
56 #[cfg(target_os = "windows")]
57 fn is_pid_active(pid: usize) -> bool {
58 use std::process::Command;
61 let output = Command::new("tasklist")
62 .arg("/FI")
63 .arg(format!("PID eq {}", pid))
64 .output()
65 .expect("Failed to execute tasklist command");
66
67 let output_str = String::from_utf8_lossy(&output.stdout);
68 output_str.contains(&format!("{}", pid))
70 }
71
72 pub fn release(&self) -> io::Result<()> {
74 if self.is_locked() {
75 Err(io::Error::new(
76 std::io::ErrorKind::Other,
77 format!(
78 "Cannot remove a dirty lock file, it is locked by another process (PID: {:#?})",
79 self.get_locker_pid()
80 ),
81 ))
82 } else {
83 self.remove_file()?;
84 Ok(())
85 }
86 }
87
88 fn remove_file(&self) -> io::Result<()> {
89 match remove_file(&self.0) {
90 Err(e) => {
91 if e.kind() != std::io::ErrorKind::NotFound {
92 return Err(e);
93 }
94 Ok(())
95 }
96 _ => Ok(()),
97 }
98 }
99
100 pub fn get_locker_pid(&self) -> Option<usize> {
103 let fs = File::open(&self.0);
104 if let Ok(mut file) = fs {
105 let mut contents = String::new();
106 file.read_to_string(&mut contents).ok();
107 drop(file);
108 if let Ok(pid) = contents.trim().parse::<usize>() {
109 return if Self::is_pid_active(pid) {
110 Some(pid)
111 } else {
112 let _ = self.remove_file();
113 None
114 };
115 }
116 }
117 None
118 }
119
120 pub fn is_locked(&self) -> bool {
123 self.get_locker_pid()
124 .map(|pid| pid != (std::process::id() as usize))
125 .unwrap_or_default()
126 }
127
128 pub fn lock(&self) -> io::Result<()> {
130 self.release()?;
131 if let Some(dir) = self.0.parent() {
132 create_dir_all(dir)?;
134 }
135
136 let mut fs = File::create(&self.0)?;
137 fs.write_all(std::process::id().to_string().as_bytes())?;
138 fs.sync_all()?;
139 fs.flush()?;
140 Ok(())
141 }
142
143 pub fn cleanup_stale_files() -> io::Result<Vec<PathBuf>> {
146 let lock_dir = user_forc_directory().join(".lsp-locks");
147 let entries = read_dir(&lock_dir)?;
148 let mut cleaned_paths = Vec::new();
149
150 for entry in entries {
151 let entry = entry?;
152 let path = entry.path();
153 if let Some(ext) = path.extension().and_then(|ext| ext.to_str()) {
154 if ext == "lock" {
155 if let Ok(mut file) = File::open(&path) {
156 let mut contents = String::new();
157 if file.read_to_string(&mut contents).is_ok() {
158 if let Ok(pid) = contents.trim().parse::<usize>() {
159 if !Self::is_pid_active(pid) {
160 remove_file(&path)?;
161 cleaned_paths.push(path);
162 }
163 } else {
164 remove_file(&path)?;
165 cleaned_paths.push(path);
166 }
167 }
168 }
169 }
170 }
171 }
172 Ok(cleaned_paths)
173 }
174}
175
176pub fn is_file_dirty<X: AsRef<Path>>(path: X) -> bool {
182 PidFileLocking::lsp(path.as_ref()).is_locked()
183}
184
185#[cfg(test)]
186mod test {
187 use super::{user_forc_directory, PidFileLocking};
188 use mark_flaky_tests::flaky;
189 use std::{
190 fs::{metadata, File},
191 io::{ErrorKind, Write},
192 os::unix::fs::MetadataExt,
193 };
194
195 #[test]
196 fn test_fs_locking_same_process() {
197 let x = PidFileLocking::lsp("test");
198 assert!(!x.is_locked()); assert!(x.lock().is_ok());
200 let x = PidFileLocking::lsp("test");
202 assert!(!x.is_locked());
203 }
204
205 #[test]
206 fn test_legacy() {
207 let x = PidFileLocking::lsp("legacy");
209 assert!(x.lock().is_ok());
210 assert!(metadata(&x.0).is_ok());
212
213 let _ = File::create(&x.0).unwrap();
215 assert_eq!(metadata(&x.0).unwrap().size(), 0);
216
217 let x = PidFileLocking::lsp("legacy");
218 assert!(!x.is_locked());
219 }
220
221 #[test]
222 fn test_remove() {
223 let x = PidFileLocking::lsp("lock");
224 assert!(x.lock().is_ok());
225 assert!(x.release().is_ok());
226 assert!(x.release().is_ok());
227 }
228
229 #[test]
230 fn test_fs_locking_stale() {
231 let x = PidFileLocking::lsp("stale");
232 assert!(x.lock().is_ok());
233
234 assert!(metadata(&x.0).is_ok());
236
237 let mut x = File::create(&x.0).unwrap();
239 x.write_all(b"191919191919").unwrap();
240 x.flush().unwrap();
241 drop(x);
242
243 let x = PidFileLocking::lsp("stale");
245 assert!(!x.is_locked());
246 let e = metadata(&x.0).unwrap_err().kind();
247 assert_eq!(e, ErrorKind::NotFound);
248 }
249
250 #[flaky]
251 #[test]
252 fn test_cleanup_stale_files() {
253 let test_lock = PidFileLocking::lsp("test_cleanup");
255 test_lock.lock().expect("Failed to create test lock file");
256
257 let lock_path = user_forc_directory()
259 .join(".lsp-locks")
260 .join("test_cleanup_invalid.lock");
261
262 {
264 let mut file = File::create(&lock_path).expect("Failed to create test lock file");
265 file.write_all(b"not-a-pid")
266 .expect("Failed to write invalid content");
267 file.flush().expect("Failed to flush file");
268 }
269
270 assert!(
272 test_lock.0.exists(),
273 "Valid lock file should exist before cleanup"
274 );
275 assert!(
276 lock_path.exists(),
277 "Invalid lock file should exist before cleanup"
278 );
279
280 let cleaned_paths =
282 PidFileLocking::cleanup_stale_files().expect("Failed to cleanup stale files");
283
284 assert_eq!(cleaned_paths.len(), 1, "Expected one file to be cleaned up");
286 assert_eq!(
287 cleaned_paths[0], lock_path,
288 "Expected invalid file to be cleaned up"
289 );
290
291 assert!(test_lock.0.exists(), "Active lock file should still exist");
293 assert!(!lock_path.exists(), "Lock file should be removed");
294
295 test_lock.release().expect("Failed to release test lock");
297 }
298}