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::other(format!(
76 "Cannot remove a dirty lock file, it is locked by another process (PID: {:#?})",
77 self.get_locker_pid()
78 )))
79 } else {
80 self.remove_file()?;
81 Ok(())
82 }
83 }
84
85 fn remove_file(&self) -> io::Result<()> {
86 match remove_file(&self.0) {
87 Err(e) => {
88 if e.kind() != std::io::ErrorKind::NotFound {
89 return Err(e);
90 }
91 Ok(())
92 }
93 _ => Ok(()),
94 }
95 }
96
97 pub fn get_locker_pid(&self) -> Option<usize> {
100 let fs = File::open(&self.0);
101 if let Ok(mut file) = fs {
102 let mut contents = String::new();
103 file.read_to_string(&mut contents).ok();
104 drop(file);
105 if let Ok(pid) = contents.trim().parse::<usize>() {
106 return if Self::is_pid_active(pid) {
107 Some(pid)
108 } else {
109 let _ = self.remove_file();
110 None
111 };
112 }
113 }
114 None
115 }
116
117 pub fn is_locked(&self) -> bool {
120 self.get_locker_pid()
121 .map(|pid| pid != (std::process::id() as usize))
122 .unwrap_or_default()
123 }
124
125 pub fn lock(&self) -> io::Result<()> {
127 self.release()?;
128 if let Some(dir) = self.0.parent() {
129 create_dir_all(dir)?;
131 }
132
133 let mut fs = File::create(&self.0)?;
134 fs.write_all(std::process::id().to_string().as_bytes())?;
135 fs.sync_all()?;
136 fs.flush()?;
137 Ok(())
138 }
139
140 pub fn cleanup_stale_files() -> io::Result<Vec<PathBuf>> {
143 let lock_dir = user_forc_directory().join(".lsp-locks");
144 let entries = read_dir(&lock_dir)?;
145 let mut cleaned_paths = Vec::new();
146
147 for entry in entries {
148 let entry = entry?;
149 let path = entry.path();
150 if let Some(ext) = path.extension().and_then(|ext| ext.to_str()) {
151 if ext == "lock" {
152 if let Ok(mut file) = File::open(&path) {
153 let mut contents = String::new();
154 if file.read_to_string(&mut contents).is_ok() {
155 if let Ok(pid) = contents.trim().parse::<usize>() {
156 if !Self::is_pid_active(pid) {
157 remove_file(&path)?;
158 cleaned_paths.push(path);
159 }
160 } else {
161 remove_file(&path)?;
162 cleaned_paths.push(path);
163 }
164 }
165 }
166 }
167 }
168 }
169 Ok(cleaned_paths)
170 }
171}
172
173pub fn is_file_dirty<X: AsRef<Path>>(path: X) -> bool {
179 PidFileLocking::lsp(path.as_ref()).is_locked()
180}
181
182#[cfg(test)]
183mod test {
184 use super::{user_forc_directory, PidFileLocking};
185 use mark_flaky_tests::flaky;
186 use std::{
187 fs::{metadata, File},
188 io::{ErrorKind, Write},
189 os::unix::fs::MetadataExt,
190 };
191
192 #[test]
193 fn test_fs_locking_same_process() {
194 let x = PidFileLocking::lsp("test");
195 assert!(!x.is_locked()); assert!(x.lock().is_ok());
197 let x = PidFileLocking::lsp("test");
199 assert!(!x.is_locked());
200 }
201
202 #[test]
203 fn test_legacy() {
204 let x = PidFileLocking::lsp("legacy");
206 assert!(x.lock().is_ok());
207 assert!(metadata(&x.0).is_ok());
209
210 let _ = File::create(&x.0).unwrap();
212 assert_eq!(metadata(&x.0).unwrap().size(), 0);
213
214 let x = PidFileLocking::lsp("legacy");
215 assert!(!x.is_locked());
216 }
217
218 #[test]
219 fn test_remove() {
220 let x = PidFileLocking::lsp("lock");
221 assert!(x.lock().is_ok());
222 assert!(x.release().is_ok());
223 assert!(x.release().is_ok());
224 }
225
226 #[test]
227 fn test_fs_locking_stale() {
228 let x = PidFileLocking::lsp("stale");
229 assert!(x.lock().is_ok());
230
231 assert!(metadata(&x.0).is_ok());
233
234 let mut x = File::create(&x.0).unwrap();
236 x.write_all(b"191919191919").unwrap();
237 x.flush().unwrap();
238 drop(x);
239
240 let x = PidFileLocking::lsp("stale");
242 assert!(!x.is_locked());
243 let e = metadata(&x.0).unwrap_err().kind();
244 assert_eq!(e, ErrorKind::NotFound);
245 }
246
247 #[flaky]
248 #[test]
249 fn test_cleanup_stale_files() {
250 let test_lock = PidFileLocking::lsp("test_cleanup");
252 test_lock.lock().expect("Failed to create test lock file");
253
254 let lock_path = user_forc_directory()
256 .join(".lsp-locks")
257 .join("test_cleanup_invalid.lock");
258
259 {
261 let mut file = File::create(&lock_path).expect("Failed to create test lock file");
262 file.write_all(b"not-a-pid")
263 .expect("Failed to write invalid content");
264 file.flush().expect("Failed to flush file");
265 }
266
267 assert!(
269 test_lock.0.exists(),
270 "Valid lock file should exist before cleanup"
271 );
272 assert!(
273 lock_path.exists(),
274 "Invalid lock file should exist before cleanup"
275 );
276
277 let cleaned_paths =
279 PidFileLocking::cleanup_stale_files().expect("Failed to cleanup stale files");
280
281 assert_eq!(cleaned_paths.len(), 1, "Expected one file to be cleaned up");
283 assert_eq!(
284 cleaned_paths[0], lock_path,
285 "Expected invalid file to be cleaned up"
286 );
287
288 assert!(test_lock.0.exists(), "Active lock file should still exist");
290 assert!(!lock_path.exists(), "Lock file should be removed");
291
292 test_lock.release().expect("Failed to release test lock");
294 }
295}