forc_util/
fs_locking.rs

1use crate::{hash_path, user_forc_directory};
2use std::{
3    fs::{create_dir_all, remove_file, File},
4    io::{self, Read, Write},
5    path::{Path, PathBuf},
6};
7
8/// Very simple AdvisoryPathMutex class
9///
10/// The goal of this struct is to signal other processes that a path is being used by another
11/// process exclusively.
12///
13/// This struct will self-heal if the process that locked the file is no longer running.
14pub 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 file_name = hash_path(filename);
23        Self(
24            user_forc_directory()
25                .join(dir)
26                .join(file_name)
27                .with_extension(extension),
28        )
29    }
30
31    /// Create a new PidFileLocking instance that is shared between the LSP and any other process
32    /// that may want to update the file and needs to wait for the LSP to finish (like forc-fmt)
33    pub fn lsp<X: AsRef<Path>>(filename: X) -> PidFileLocking {
34        Self::new(filename, ".lsp-locks", "lock")
35    }
36
37    /// Checks if the given pid is active
38    #[cfg(not(target_os = "windows"))]
39    fn is_pid_active(pid: usize) -> bool {
40        // Not using sysinfo here because it has compatibility issues with fuel.nix
41        // https://github.com/FuelLabs/fuel.nix/issues/64
42        use std::process::Command;
43        let output = Command::new("ps")
44            .arg("-p")
45            .arg(pid.to_string())
46            .output()
47            .expect("Failed to execute ps command");
48
49        let output_str = String::from_utf8_lossy(&output.stdout);
50        output_str.contains(&format!("{} ", pid))
51    }
52
53    #[cfg(target_os = "windows")]
54    fn is_pid_active(pid: usize) -> bool {
55        // Not using sysinfo here because it has compatibility issues with fuel.nix
56        // https://github.com/FuelLabs/fuel.nix/issues/64
57        use std::process::Command;
58        let output = Command::new("tasklist")
59            .arg("/FI")
60            .arg(format!("PID eq {}", pid))
61            .output()
62            .expect("Failed to execute tasklist command");
63
64        let output_str = String::from_utf8_lossy(&output.stdout);
65        // Check if the output contains the PID, indicating the process is active
66        output_str.contains(&format!("{}", pid))
67    }
68
69    /// Removes the lock file if it is not locked or the process that locked it is no longer active
70    pub fn release(&self) -> io::Result<()> {
71        if self.is_locked() {
72            Err(io::Error::new(
73                std::io::ErrorKind::Other,
74                format!(
75                    "Cannot remove a dirty lock file, it is locked by another process (PID: {:#?})",
76                    self.get_locker_pid()
77                ),
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    /// Returns the PID of the owner of the current lock. If the PID is not longer active the lock
98    /// file will be removed
99    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    /// Checks if the current path is owned by any other process. This will return false if there is
118    /// no lock file or the current process is the owner of the lock file
119    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    /// Locks the given filepath if it is not already locked
126    pub fn lock(&self) -> io::Result<()> {
127        self.release()?;
128        if let Some(dir) = self.0.parent() {
129            // Ensure the directory exists
130            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
141/// Checks if the specified file is marked as "dirty".
142/// This is used to prevent changing files that are currently open in an editor
143/// with unsaved changes.
144///
145/// Returns `true` if a corresponding "dirty" flag file exists, `false` otherwise.
146pub fn is_file_dirty<X: AsRef<Path>>(path: X) -> bool {
147    PidFileLocking::lsp(path.as_ref()).is_locked()
148}
149
150#[cfg(test)]
151mod test {
152    use super::PidFileLocking;
153    use std::{
154        fs::{metadata, File},
155        io::{ErrorKind, Write},
156        os::unix::fs::MetadataExt,
157    };
158
159    #[test]
160    fn test_fs_locking_same_process() {
161        let x = PidFileLocking::lsp("test");
162        assert!(!x.is_locked()); // checks the non-existence of the lock (therefore it is not locked)
163        assert!(x.lock().is_ok());
164        // The current process is locking "test"
165        let x = PidFileLocking::lsp("test");
166        assert!(!x.is_locked());
167    }
168
169    #[test]
170    fn test_legacy() {
171        // tests against an empty file (as legacy were creating this files)
172        let x = PidFileLocking::lsp("legacy");
173        assert!(x.lock().is_ok());
174        // lock file exists,
175        assert!(metadata(&x.0).is_ok());
176
177        // simulate a stale lock file from legacy (which should be empty)
178        let _ = File::create(&x.0).unwrap();
179        assert_eq!(metadata(&x.0).unwrap().size(), 0);
180
181        let x = PidFileLocking::lsp("legacy");
182        assert!(!x.is_locked());
183    }
184
185    #[test]
186    fn test_remove() {
187        let x = PidFileLocking::lsp("lock");
188        assert!(x.lock().is_ok());
189        assert!(x.release().is_ok());
190        assert!(x.release().is_ok());
191    }
192
193    #[test]
194    fn test_fs_locking_stale() {
195        let x = PidFileLocking::lsp("stale");
196        assert!(x.lock().is_ok());
197
198        // lock file exists,
199        assert!(metadata(&x.0).is_ok());
200
201        // simulate a stale lock file
202        let mut x = File::create(&x.0).unwrap();
203        x.write_all(b"191919191919").unwrap();
204        x.flush().unwrap();
205        drop(x);
206
207        // PID=191919191919 does not exists, hopefully, and this should remove the lock file
208        let x = PidFileLocking::lsp("stale");
209        assert!(!x.is_locked());
210        let e = metadata(&x.0).unwrap_err().kind();
211        assert_eq!(e, ErrorKind::NotFound);
212    }
213}