From 5ac17a64ee6917c38dda95bc0f42c1945f43d121 Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Wed, 13 May 2026 11:08:13 +0200 Subject: [PATCH 1/2] Fix atomic rewatch lock acquisition --- rewatch/src/lock.rs | 74 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 18 deletions(-) diff --git a/rewatch/src/lock.rs b/rewatch/src/lock.rs index 60f0222f82..8f1d3e9280 100644 --- a/rewatch/src/lock.rs +++ b/rewatch/src/lock.rs @@ -1,7 +1,7 @@ use anyhow::Result; use notify::{Config, Event, EventKind, RecommendedWatcher, RecursiveMode, Watcher}; use std::fs; -use std::fs::File; +use std::fs::OpenOptions; use std::io::Write; use std::path::Path; use std::process; @@ -173,9 +173,24 @@ pub fn get(kind: LockKind, folder: &str) -> Lock { let location = lib_dir.join(kind.file_name()); let pid = process::id(); - // When a lockfile already exists we parse its PID: if the process is still alive we refuse to - // proceed, otherwise we will overwrite the stale lock with our own PID. loop { + if let Err(e) = fs::create_dir_all(&lib_dir) { + return Lock::Error(Error::WritingLockfile(e)); + } + + match OpenOptions::new().write(true).create_new(true).open(&location) { + Ok(mut file) => { + return match file.write(pid.to_string().as_bytes()) { + Ok(_) => Lock::Aquired(pid), + Err(e) => Lock::Error(Error::WritingLockfile(e)), + }; + } + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => (), + Err(e) => return Lock::Error(Error::WritingLockfile(e)), + } + + // When a lockfile already exists we parse its PID: if the process is still alive we refuse to + // proceed, otherwise we remove the stale lock and try the atomic create again. match fs::read_to_string(&location) { Ok(contents) => match contents.parse::() { Ok(parsed_pid) if pid_matches_current_process(parsed_pid) => match kind { @@ -188,26 +203,17 @@ pub fn get(kind: LockKind, folder: &str) -> Lock { } LockKind::Watch => return Lock::Error(Error::Locked(parsed_pid)), }, - Ok(_) => break, + Ok(_) => match fs::remove_file(&location) { + Ok(_) => continue, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue, + Err(e) => return Lock::Error(Error::ReadingLockfile(kind, e)), + }, Err(e) => return Lock::Error(Error::ParsingLockfile(e)), }, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => break, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue, Err(e) => return Lock::Error(Error::ReadingLockfile(kind, e)), } } - - if let Err(e) = fs::create_dir_all(&lib_dir) { - return Lock::Error(Error::WritingLockfile(e)); - } - - // Rewrite the lockfile with our own PID. - match File::create(&location) { - Ok(mut file) => match file.write(pid.to_string().as_bytes()) { - Ok(_) => Lock::Aquired(pid), - Err(e) => Lock::Error(Error::WritingLockfile(e)), - }, - Err(e) => Lock::Error(Error::WritingLockfile(e)), - } } pub fn get_lock_or_exit(kind: LockKind, folder: &str) -> Lock { @@ -239,6 +245,7 @@ pub fn drop_lock(kind: LockKind, folder: &str) -> Result<()> { mod tests { use super::*; use std::fs; + use std::sync::{Arc, Barrier}; use std::thread; use std::time::Duration; use tempfile::TempDir; @@ -291,6 +298,37 @@ mod tests { ); } + #[test] + fn only_one_concurrent_caller_acquires_lock() { + let temp_dir = TempDir::new().expect("temp dir should be created"); + let project_folder = temp_dir.path().join("project"); + fs::create_dir(&project_folder).expect("project folder should be created"); + + let caller_count = 12; + let start = Arc::new(Barrier::new(caller_count)); + let handles = (0..caller_count) + .map(|_| { + let start = Arc::clone(&start); + let project_folder = project_folder.clone(); + thread::spawn(move || { + start.wait(); + get( + LockKind::Watch, + project_folder.to_str().expect("path should be valid"), + ) + }) + }) + .collect::>(); + + let acquired_count = handles + .into_iter() + .map(|handle| handle.join().expect("lock thread should complete")) + .filter(|lock| matches!(lock, Lock::Aquired(_))) + .count(); + + assert_eq!(acquired_count, 1); + } + #[test] fn ignores_stale_lock_for_unrelated_process_name() { let temp_dir = TempDir::new().expect("temp dir should be created"); From b68a935cf1076c204b26355b27c74bd6d779e70a Mon Sep 17 00:00:00 2001 From: Christoph Knittel Date: Wed, 13 May 2026 13:10:56 +0200 Subject: [PATCH 2/2] Wait for rewatch test watchers to exit --- rewatch/tests/lock/01-lock-when-watching.sh | 2 ++ rewatch/tests/utils.sh | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/rewatch/tests/lock/01-lock-when-watching.sh b/rewatch/tests/lock/01-lock-when-watching.sh index c97706f481..a25f439510 100755 --- a/rewatch/tests/lock/01-lock-when-watching.sh +++ b/rewatch/tests/lock/01-lock-when-watching.sh @@ -92,3 +92,5 @@ else cat rewatch.log exit 1 fi + +exit_watcher diff --git a/rewatch/tests/utils.sh b/rewatch/tests/utils.sh index 0955c2681d..eee37596b5 100644 --- a/rewatch/tests/utils.sh +++ b/rewatch/tests/utils.sh @@ -51,8 +51,29 @@ replace() { fi } +wait_for_pid_gone() { + local pid="$1"; local timeout="${2:-10}" + while kill -0 "$pid" 2> /dev/null && [ "$timeout" -gt 0 ]; do + sleep 1 + timeout=$((timeout - 1)) + done + ! kill -0 "$pid" 2> /dev/null +} + exit_watcher() { + local watcher_pid="" + if [ -f lib/watch.lock ]; then + watcher_pid=$(cat lib/watch.lock) + fi + rm -f lib/watch.lock + + if [ -n "$watcher_pid" ]; then + if ! wait_for_pid_gone "$watcher_pid" 10; then + error "Watcher process $watcher_pid did not exit after watch.lock was removed" + return 1 + fi + fi } clear_locks() {