Skip to content

Commit

Permalink
Implement Pid file locking
Browse files Browse the repository at this point in the history
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 to prevent future instances
  • Loading branch information
crodas committed Feb 28, 2024
1 parent 4d5df24 commit 9ef6186
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 49 deletions.
30 changes: 28 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 7 additions & 4 deletions forc-plugins/forc-fmt/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ use forc_pkg::{
manifest::{GenericManifestFile, ManifestFile},
WorkspaceManifestFile,
};
use forc_util::fs_locking::PidFileLocking;
use prettydiff::{basic::DiffOp, diff_lines};
use std::{
default::Default,
Expand Down Expand Up @@ -51,7 +52,8 @@ pub struct App {
pub path: Option<String>,
#[clap(short, long)]
/// Formats a single .sw file with the default settings.
/// If not specified, current working directory will be formatted using a Forc.toml configuration.
/// If not specified, current working directory will be formatted using a Forc.toml
/// configuration.
pub file: Option<String>,
}

Expand Down Expand Up @@ -109,9 +111,10 @@ fn run() -> Result<()> {
/// with unsaved changes.
///
/// Returns `true` if a corresponding "dirty" flag file exists, `false` otherwise.
fn is_file_dirty(path: &Path) -> bool {
let dirty_file_path = forc_util::is_dirty_path(path);
dirty_file_path.exists()
fn is_file_dirty<X: AsRef<Path>>(path: X) -> bool {
PidFileLocking::lsp(path.as_ref())
.is_locked()
.unwrap_or(false)
}

/// Recursively get a Vec<PathBuf> of subdirectories that contains a Forc.toml.
Expand Down
1 change: 1 addition & 0 deletions forc-util/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ sway-core = { version = "0.51.1", path = "../sway-core" }
sway-error = { version = "0.51.1", path = "../sway-error" }
sway-types = { version = "0.51.1", path = "../sway-types" }
sway-utils = { version = "0.51.1", path = "../sway-utils" }
sysinfo = "0.30.5"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = [
"ansi",
Expand Down
143 changes: 143 additions & 0 deletions forc-util/src/fs_locking.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
use crate::{hash_path, user_forc_directory};
use std::{
fs::{create_dir_all, remove_file, File},
io::{self, Read, Write},
path::{Path, PathBuf},
};

/// Very simple AdvisoryPathMutex class
///
/// The goal of this struct is to signal other processes that a path is being used by another
/// process exclusively.
///
/// This struct will self-healh if the process that locked the file is no longer running.
pub struct PidFileLocking(PathBuf);

impl PidFileLocking {
pub fn new<X: AsRef<Path>, Y: AsRef<Path>>(path: X, dir: Y, extension: &str) -> PidFileLocking {
let file_name = hash_path(path);
Self(
user_forc_directory()
.join(dir)
.join(file_name)
.with_extension(extension),
)
}

pub fn lsp<X: AsRef<Path>>(path: X) -> PidFileLocking {
Self::new(path, ".lsp-locks", "dirty")
}

fn is_pid_active(pid: usize) -> bool {
use sysinfo::{Pid, System};
if pid == std::process::id() as usize {
return false;
}
System::new_all().process(Pid::from(pid)).is_some()
}

pub fn remove(&self) -> io::Result<()> {
if self.is_locked()? {
Err(io::Error::new(
std::io::ErrorKind::Other,
"Cannot remove a dirty lock file, it is locked by another process",
))
} else {
self.remove_file()
}
}

fn remove_file(&self) -> io::Result<()> {
match remove_file(&self.0) {
Err(error) => {
if error.kind() == io::ErrorKind::NotFound {
Ok(())
} else {
Err(error)
}
}
_ => Ok(()),
}
}

pub fn is_locked(&self) -> io::Result<bool> {
let fs = File::open(&self.0);
println!("{:#?}", fs);
match fs {
Ok(mut file) => {
let mut pid = String::new();
file.read_to_string(&mut pid)?;
let is_locked = pid
.trim()
.parse::<usize>()
.map(|x| Self::is_pid_active(x))
.unwrap_or_default();
drop(file);
if !is_locked {
self.remove_file()?;
}
Ok(is_locked)
}
Err(err) => {
if err.kind() == io::ErrorKind::NotFound {
Ok(false)
} else {
Err(err)
}
}
}
}

pub fn lock(&self) -> io::Result<()> {
self.remove()?;
if let Some(dir) = self.0.parent() {
// Ensure the directory exists
create_dir_all(dir)?;
}

let mut fs = File::create(&self.0)?;
fs.write_all(&std::process::id().to_string().as_bytes())?;
fs.sync_all()?;
fs.flush()?;
Ok(())
}
}

#[cfg(test)]
mod test {
use super::PidFileLocking;
use std::{
fs::{metadata, File},
io::{ErrorKind, Write},
};

#[test]
fn same_process() {
let x = PidFileLocking::lsp("test");
assert!(x.lock().is_ok());
// The current process is locking "test"
let x = PidFileLocking::lsp("test");
assert!(!x.is_locked().unwrap());
}

#[test]
fn stale() {
let x = PidFileLocking::lsp("stale");
assert!(x.lock().is_ok());

// lock file exists,
assert!(metadata(&x.0).is_ok());

// simulate a stale lock file
let mut x = File::create(&x.0).unwrap();
x.write_all(b"191919191919").unwrap();
x.flush().unwrap();
drop(x);

// PID=191919191919 does not exists, hopefully, and this should remove the lock file
let x = PidFileLocking::lsp("test");
assert!(!x.is_locked().unwrap());
let e = metadata(&x.0).unwrap_err().kind();
assert_eq!(e, ErrorKind::NotFound);
}
}
25 changes: 7 additions & 18 deletions forc-util/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
//! Utility items shared between forc crates.
use annotate_snippets::{
renderer::{AnsiColor, Style},
Annotation, AnnotationType, Renderer, Slice, Snippet, SourceAnnotation,
Expand All @@ -26,6 +25,7 @@ use sway_types::{LineCol, SourceEngine, Span};
use sway_utils::constants;
use tracing::error;

pub mod fs_locking;
pub mod restricted;

#[macro_use]
Expand Down Expand Up @@ -156,7 +156,8 @@ pub mod tx_utils {
pub struct Salt {
/// Added salt used to derive the contract ID.
///
/// By default, this is `0x0000000000000000000000000000000000000000000000000000000000000000`.
/// By default, this is
/// `0x0000000000000000000000000000000000000000000000000000000000000000`.
#[clap(long = "salt")]
pub salt: Option<fuel_tx::Salt>,
}
Expand Down Expand Up @@ -288,7 +289,7 @@ pub fn git_checkouts_directory() -> PathBuf {
///
/// Note: This has nothing to do with `Forc.lock` files, rather this is about fd locks for
/// coordinating access to particular paths (e.g. git checkout directories).
fn fd_lock_path(path: &Path) -> PathBuf {
fn fd_lock_path<X: AsRef<Path>>(path: X) -> PathBuf {
const LOCKS_DIR_NAME: &str = ".locks";
const LOCK_EXT: &str = "forc-lock";
let file_name = hash_path(path);
Expand All @@ -298,22 +299,10 @@ fn fd_lock_path(path: &Path) -> PathBuf {
.with_extension(LOCK_EXT)
}

/// Constructs the path for the "dirty" flag file corresponding to the specified file.
///
/// This function uses a hashed representation of the original path for uniqueness.
pub fn is_dirty_path(path: &Path) -> PathBuf {
const LOCKS_DIR_NAME: &str = ".lsp-locks";
const LOCK_EXT: &str = "dirty";
let file_name = hash_path(path);
user_forc_directory()
.join(LOCKS_DIR_NAME)
.join(file_name)
.with_extension(LOCK_EXT)
}

/// Hash the path to produce a file-system friendly file name.
/// Append the file stem for improved readability.
fn hash_path(path: &Path) -> String {
fn hash_path<X: AsRef<Path>>(path: X) -> String {
let path = path.as_ref();
let mut hasher = hash_map::DefaultHasher::default();
path.hash(&mut hasher);
let hash = hasher.finish();
Expand All @@ -327,7 +316,7 @@ fn hash_path(path: &Path) -> String {
/// Create an advisory lock over the given path.
///
/// See [fd_lock_path] for details.
pub fn path_lock(path: &Path) -> Result<fd_lock::RwLock<File>> {
pub fn path_lock<X: AsRef<Path>>(path: X) -> Result<fd_lock::RwLock<File>> {
let lock_path = fd_lock_path(path);
let lock_dir = lock_path
.parent()
Expand Down
42 changes: 17 additions & 25 deletions sway-lsp/src/core/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ use crate::{
error::{DirectoryError, DocumentError, LanguageServerError},
utils::document,
};
use forc_util::fs_locking::PidFileLocking;
use lsp_types::{Position, Range, TextDocumentContentChangeEvent, Url};
use ropey::Rope;
use tokio::fs::File;

#[derive(Debug, Clone)]
pub struct TextDocument {
Expand Down Expand Up @@ -113,39 +113,31 @@ impl TextDocument {
/// This function ensures the necessary directory structure exists before creating the flag file.
pub async fn mark_file_as_dirty(uri: &Url) -> Result<(), LanguageServerError> {
let path = document::get_path_from_url(uri)?;
let dirty_file_path = forc_util::is_dirty_path(&path);
if let Some(dir) = dirty_file_path.parent() {
// Ensure the directory exists
tokio::fs::create_dir_all(dir)
.await
.map_err(|_| DirectoryError::LspLocksDirFailed)?;
}
// Create an empty "dirty" file
File::create(&dirty_file_path)
.await
.map_err(|err| DocumentError::UnableToCreateFile {
path: uri.path().to_string(),
err: err.to_string(),
})?;
Ok(())
tokio::task::spawn_blocking(move || {
Ok(PidFileLocking::lsp(&path)
.lock()
.map_err(|_| DirectoryError::LspLocksDirFailed)?)
})
.await
.map_err(|_| DirectoryError::LspLocksDirFailed)?
}

/// Removes the corresponding flag file for the specifed Url.
///
/// If the flag file does not exist, this function will do nothing.
pub async fn remove_dirty_flag(uri: &Url) -> Result<(), LanguageServerError> {
let path = document::get_path_from_url(uri)?;
let dirty_file_path = forc_util::is_dirty_path(&path);
if dirty_file_path.exists() {
// Remove the "dirty" file
tokio::fs::remove_file(dirty_file_path)
.await
.map_err(|err| DocumentError::UnableToRemoveFile {
let uri = uri.clone();
tokio::task::spawn_blocking(move || {
Ok(PidFileLocking::lsp(&path).remove().map_err(|err| {
DocumentError::UnableToRemoveFile {
path: uri.path().to_string(),
err: err.to_string(),
})?;
}
Ok(())
}
})?)
})
.await
.map_err(|_| DirectoryError::LspLocksDirFailed)?
}

#[derive(Debug)]
Expand Down

0 comments on commit 9ef6186

Please sign in to comment.