Skip to content

Commit 40dcbbb

Browse files
committed
Implement Pid file locking
Fixes #5633 Added functionality to create and manage a lock file containing the process ID (pid) of the running instance of the software. This mechanism prevents multiple instances of the software from running simultaneously by checking the existence and content of the lock file. If the lock file exists and contains a valid pid, the struct will error gracefully to avoid conflicts. If the lock file is missing or contains an invalid pid, the struct will proceed by removing the file. This ensures that only one instance of the software can run at a time and it avoids stale locking for preventing future instances This implements an advisory mutex based on path. This mutex is advisory and opt-in. It is a cheap way to multiple processes to avoid writing the same file at the same time
1 parent 4d5df24 commit 40dcbbb

File tree

6 files changed

+203
-49
lines changed

6 files changed

+203
-49
lines changed

Cargo.lock

Lines changed: 28 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

forc-plugins/forc-fmt/src/main.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use forc_pkg::{
66
manifest::{GenericManifestFile, ManifestFile},
77
WorkspaceManifestFile,
88
};
9+
use forc_util::fs_locking::PidFileLocking;
910
use prettydiff::{basic::DiffOp, diff_lines};
1011
use std::{
1112
default::Default,
@@ -51,7 +52,8 @@ pub struct App {
5152
pub path: Option<String>,
5253
#[clap(short, long)]
5354
/// Formats a single .sw file with the default settings.
54-
/// If not specified, current working directory will be formatted using a Forc.toml configuration.
55+
/// If not specified, current working directory will be formatted using a Forc.toml
56+
/// configuration.
5557
pub file: Option<String>,
5658
}
5759

@@ -109,9 +111,10 @@ fn run() -> Result<()> {
109111
/// with unsaved changes.
110112
///
111113
/// Returns `true` if a corresponding "dirty" flag file exists, `false` otherwise.
112-
fn is_file_dirty(path: &Path) -> bool {
113-
let dirty_file_path = forc_util::is_dirty_path(path);
114-
dirty_file_path.exists()
114+
fn is_file_dirty<X: AsRef<Path>>(path: X) -> bool {
115+
PidFileLocking::lsp(path.as_ref())
116+
.is_locked()
117+
.unwrap_or(false)
115118
}
116119

117120
/// Recursively get a Vec<PathBuf> of subdirectories that contains a Forc.toml.

forc-util/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ sway-core = { version = "0.51.1", path = "../sway-core" }
2727
sway-error = { version = "0.51.1", path = "../sway-error" }
2828
sway-types = { version = "0.51.1", path = "../sway-types" }
2929
sway-utils = { version = "0.51.1", path = "../sway-utils" }
30+
sysinfo = "0.30.5"
3031
tracing = "0.1"
3132
tracing-subscriber = { version = "0.3", features = [
3233
"ansi",

forc-util/src/fs_locking.rs

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
use crate::{hash_path, user_forc_directory};
2+
use 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-healh if the process that locked the file is no longer running.
14+
pub struct PidFileLocking(PathBuf);
15+
16+
impl PidFileLocking {
17+
pub fn new<X: AsRef<Path>, Y: AsRef<Path>>(path: X, dir: Y, extension: &str) -> PidFileLocking {
18+
let file_name = hash_path(path);
19+
Self(
20+
user_forc_directory()
21+
.join(dir)
22+
.join(file_name)
23+
.with_extension(extension),
24+
)
25+
}
26+
27+
pub fn lsp<X: AsRef<Path>>(path: X) -> PidFileLocking {
28+
Self::new(path, ".lsp-locks", "dirty")
29+
}
30+
31+
fn is_pid_active(pid: usize) -> bool {
32+
use sysinfo::{Pid, System};
33+
if pid == std::process::id() as usize {
34+
return false;
35+
}
36+
System::new_all().process(Pid::from(pid)).is_some()
37+
}
38+
39+
pub fn remove(&self) -> io::Result<()> {
40+
if self.is_locked()? {
41+
Err(io::Error::new(
42+
std::io::ErrorKind::Other,
43+
"Cannot remove a dirty lock file, it is locked by another process",
44+
))
45+
} else {
46+
self.remove_file()
47+
}
48+
}
49+
50+
fn remove_file(&self) -> io::Result<()> {
51+
match remove_file(&self.0) {
52+
Err(error) => {
53+
if error.kind() == io::ErrorKind::NotFound {
54+
Ok(())
55+
} else {
56+
Err(error)
57+
}
58+
}
59+
_ => Ok(()),
60+
}
61+
}
62+
63+
pub fn is_locked(&self) -> io::Result<bool> {
64+
let fs = File::open(&self.0);
65+
println!("{:#?}", fs);
66+
match fs {
67+
Ok(mut file) => {
68+
let mut pid = String::new();
69+
file.read_to_string(&mut pid)?;
70+
let is_locked = pid
71+
.trim()
72+
.parse::<usize>()
73+
.map(|x| Self::is_pid_active(x))
74+
.unwrap_or_default();
75+
drop(file);
76+
if !is_locked {
77+
self.remove_file()?;
78+
}
79+
Ok(is_locked)
80+
}
81+
Err(err) => {
82+
if err.kind() == io::ErrorKind::NotFound {
83+
Ok(false)
84+
} else {
85+
Err(err)
86+
}
87+
}
88+
}
89+
}
90+
91+
pub fn lock(&self) -> io::Result<()> {
92+
self.remove()?;
93+
if let Some(dir) = self.0.parent() {
94+
// Ensure the directory exists
95+
create_dir_all(dir)?;
96+
}
97+
98+
let mut fs = File::create(&self.0)?;
99+
fs.write_all(&std::process::id().to_string().as_bytes())?;
100+
fs.sync_all()?;
101+
fs.flush()?;
102+
Ok(())
103+
}
104+
}
105+
106+
#[cfg(test)]
107+
mod test {
108+
use super::PidFileLocking;
109+
use std::{
110+
fs::{metadata, File},
111+
io::{ErrorKind, Write},
112+
};
113+
114+
#[test]
115+
fn same_process() {
116+
let x = PidFileLocking::lsp("test");
117+
assert!(x.lock().is_ok());
118+
// The current process is locking "test"
119+
let x = PidFileLocking::lsp("test");
120+
assert!(!x.is_locked().unwrap());
121+
}
122+
123+
#[test]
124+
fn stale() {
125+
let x = PidFileLocking::lsp("stale");
126+
assert!(x.lock().is_ok());
127+
128+
// lock file exists,
129+
assert!(metadata(&x.0).is_ok());
130+
131+
// simulate a stale lock file
132+
let mut x = File::create(&x.0).unwrap();
133+
x.write_all(b"191919191919").unwrap();
134+
x.flush().unwrap();
135+
drop(x);
136+
137+
// PID=191919191919 does not exists, hopefully, and this should remove the lock file
138+
let x = PidFileLocking::lsp("test");
139+
assert!(!x.is_locked().unwrap());
140+
let e = metadata(&x.0).unwrap_err().kind();
141+
assert_eq!(e, ErrorKind::NotFound);
142+
}
143+
}

forc-util/src/lib.rs

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
//! Utility items shared between forc crates.
2-
32
use annotate_snippets::{
43
renderer::{AnsiColor, Style},
54
Annotation, AnnotationType, Renderer, Slice, Snippet, SourceAnnotation,
@@ -26,6 +25,7 @@ use sway_types::{LineCol, SourceEngine, Span};
2625
use sway_utils::constants;
2726
use tracing::error;
2827

28+
pub mod fs_locking;
2929
pub mod restricted;
3030

3131
#[macro_use]
@@ -156,7 +156,8 @@ pub mod tx_utils {
156156
pub struct Salt {
157157
/// Added salt used to derive the contract ID.
158158
///
159-
/// By default, this is `0x0000000000000000000000000000000000000000000000000000000000000000`.
159+
/// By default, this is
160+
/// `0x0000000000000000000000000000000000000000000000000000000000000000`.
160161
#[clap(long = "salt")]
161162
pub salt: Option<fuel_tx::Salt>,
162163
}
@@ -288,7 +289,7 @@ pub fn git_checkouts_directory() -> PathBuf {
288289
///
289290
/// Note: This has nothing to do with `Forc.lock` files, rather this is about fd locks for
290291
/// coordinating access to particular paths (e.g. git checkout directories).
291-
fn fd_lock_path(path: &Path) -> PathBuf {
292+
fn fd_lock_path<X: AsRef<Path>>(path: X) -> PathBuf {
292293
const LOCKS_DIR_NAME: &str = ".locks";
293294
const LOCK_EXT: &str = "forc-lock";
294295
let file_name = hash_path(path);
@@ -298,22 +299,10 @@ fn fd_lock_path(path: &Path) -> PathBuf {
298299
.with_extension(LOCK_EXT)
299300
}
300301

301-
/// Constructs the path for the "dirty" flag file corresponding to the specified file.
302-
///
303-
/// This function uses a hashed representation of the original path for uniqueness.
304-
pub fn is_dirty_path(path: &Path) -> PathBuf {
305-
const LOCKS_DIR_NAME: &str = ".lsp-locks";
306-
const LOCK_EXT: &str = "dirty";
307-
let file_name = hash_path(path);
308-
user_forc_directory()
309-
.join(LOCKS_DIR_NAME)
310-
.join(file_name)
311-
.with_extension(LOCK_EXT)
312-
}
313-
314302
/// Hash the path to produce a file-system friendly file name.
315303
/// Append the file stem for improved readability.
316-
fn hash_path(path: &Path) -> String {
304+
fn hash_path<X: AsRef<Path>>(path: X) -> String {
305+
let path = path.as_ref();
317306
let mut hasher = hash_map::DefaultHasher::default();
318307
path.hash(&mut hasher);
319308
let hash = hasher.finish();
@@ -327,7 +316,7 @@ fn hash_path(path: &Path) -> String {
327316
/// Create an advisory lock over the given path.
328317
///
329318
/// See [fd_lock_path] for details.
330-
pub fn path_lock(path: &Path) -> Result<fd_lock::RwLock<File>> {
319+
pub fn path_lock<X: AsRef<Path>>(path: X) -> Result<fd_lock::RwLock<File>> {
331320
let lock_path = fd_lock_path(path);
332321
let lock_dir = lock_path
333322
.parent()

sway-lsp/src/core/document.rs

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ use crate::{
33
error::{DirectoryError, DocumentError, LanguageServerError},
44
utils::document,
55
};
6+
use forc_util::fs_locking::PidFileLocking;
67
use lsp_types::{Position, Range, TextDocumentContentChangeEvent, Url};
78
use ropey::Rope;
8-
use tokio::fs::File;
99

1010
#[derive(Debug, Clone)]
1111
pub struct TextDocument {
@@ -113,39 +113,31 @@ impl TextDocument {
113113
/// This function ensures the necessary directory structure exists before creating the flag file.
114114
pub async fn mark_file_as_dirty(uri: &Url) -> Result<(), LanguageServerError> {
115115
let path = document::get_path_from_url(uri)?;
116-
let dirty_file_path = forc_util::is_dirty_path(&path);
117-
if let Some(dir) = dirty_file_path.parent() {
118-
// Ensure the directory exists
119-
tokio::fs::create_dir_all(dir)
120-
.await
121-
.map_err(|_| DirectoryError::LspLocksDirFailed)?;
122-
}
123-
// Create an empty "dirty" file
124-
File::create(&dirty_file_path)
125-
.await
126-
.map_err(|err| DocumentError::UnableToCreateFile {
127-
path: uri.path().to_string(),
128-
err: err.to_string(),
129-
})?;
130-
Ok(())
116+
tokio::task::spawn_blocking(move || {
117+
Ok(PidFileLocking::lsp(&path)
118+
.lock()
119+
.map_err(|_| DirectoryError::LspLocksDirFailed)?)
120+
})
121+
.await
122+
.map_err(|_| DirectoryError::LspLocksDirFailed)?
131123
}
132124

133125
/// Removes the corresponding flag file for the specifed Url.
134126
///
135127
/// If the flag file does not exist, this function will do nothing.
136128
pub async fn remove_dirty_flag(uri: &Url) -> Result<(), LanguageServerError> {
137129
let path = document::get_path_from_url(uri)?;
138-
let dirty_file_path = forc_util::is_dirty_path(&path);
139-
if dirty_file_path.exists() {
140-
// Remove the "dirty" file
141-
tokio::fs::remove_file(dirty_file_path)
142-
.await
143-
.map_err(|err| DocumentError::UnableToRemoveFile {
130+
let uri = uri.clone();
131+
tokio::task::spawn_blocking(move || {
132+
Ok(PidFileLocking::lsp(&path).remove().map_err(|err| {
133+
DocumentError::UnableToRemoveFile {
144134
path: uri.path().to_string(),
145135
err: err.to_string(),
146-
})?;
147-
}
148-
Ok(())
136+
}
137+
})?)
138+
})
139+
.await
140+
.map_err(|_| DirectoryError::LspLocksDirFailed)?
149141
}
150142

151143
#[derive(Debug)]

0 commit comments

Comments
 (0)