From 2265e711e76f024ab7667469765ce58c8ac7e0e5 Mon Sep 17 00:00:00 2001 From: Tristan Muscat Date: Thu, 26 Mar 2026 21:37:39 +0100 Subject: [PATCH 1/4] fix(gui): Change app.lock location Tries to write app.lock to $XDG_RUNTIME_DIR/nmrs/app.lock Falls back to $XDG_DATA_DIR/nmrs/app.lock --- nmrs-gui/src/file_lock.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/nmrs-gui/src/file_lock.rs b/nmrs-gui/src/file_lock.rs index 49cef55e..857875e7 100644 --- a/nmrs-gui/src/file_lock.rs +++ b/nmrs-gui/src/file_lock.rs @@ -1,15 +1,26 @@ use fs2::FileExt; +use std::fs; use std::fs::File; pub fn acquire_app_lock() -> Result { - let mut lock_path = dirs::data_local_dir().unwrap_or(std::env::temp_dir()); - lock_path.push("my_app.lock"); + // Prefer XDG_RUNTIME_DIR for ephemeral lock files (should be /run/usr/). Fall back to local data dir. + let mut lock_dir = dirs::runtime_dir() + .or_else(|| dirs::data_local_dir()) + .unwrap_or(std::env::temp_dir()); - let file = File::create(&lock_path).map_err(|e| format!("Failed to create lock file: {e}"))?; + lock_dir.push("nmrs"); + + fs::create_dir_all(&lock_dir) + .map_err(|e| format!("Failed to create lock directory: {e}"))?; + + let lock_path = lock_dir.join("app.lock"); + + let file = File::create(&lock_path) + .map_err(|e| format!("Failed to create lock file: {e}"))?; // Exclusive lock; fails if another instance holds it file.try_lock_exclusive() .map_err(|_| "Another instance is already running".to_string())?; Ok(file) -} +} \ No newline at end of file From 3eccfef43fed15484ed1251c6f9911b277fb3ab0 Mon Sep 17 00:00:00 2001 From: Tristan Muscat Date: Thu, 26 Mar 2026 23:15:28 +0100 Subject: [PATCH 2/4] style: fix code to comply with format specifications --- nmrs-gui/src/file_lock.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/nmrs-gui/src/file_lock.rs b/nmrs-gui/src/file_lock.rs index 857875e7..c6c54fa2 100644 --- a/nmrs-gui/src/file_lock.rs +++ b/nmrs-gui/src/file_lock.rs @@ -5,22 +5,20 @@ use std::fs::File; pub fn acquire_app_lock() -> Result { // Prefer XDG_RUNTIME_DIR for ephemeral lock files (should be /run/usr/). Fall back to local data dir. let mut lock_dir = dirs::runtime_dir() - .or_else(|| dirs::data_local_dir()) + .or_else(dirs::data_local_dir) .unwrap_or(std::env::temp_dir()); lock_dir.push("nmrs"); - fs::create_dir_all(&lock_dir) - .map_err(|e| format!("Failed to create lock directory: {e}"))?; + fs::create_dir_all(&lock_dir).map_err(|e| format!("Failed to create lock directory: {e}"))?; let lock_path = lock_dir.join("app.lock"); - let file = File::create(&lock_path) - .map_err(|e| format!("Failed to create lock file: {e}"))?; + let file = File::create(&lock_path).map_err(|e| format!("Failed to create lock file: {e}"))?; // Exclusive lock; fails if another instance holds it file.try_lock_exclusive() .map_err(|_| "Another instance is already running".to_string())?; Ok(file) -} \ No newline at end of file +} From 3202db68ad2d6ac17ff68369bf91b832014fb045 Mon Sep 17 00:00:00 2001 From: Tristan Muscat Date: Fri, 27 Mar 2026 12:18:54 +0100 Subject: [PATCH 3/4] test: test the `acquire_app_lock` function Adds two tests - A test to validate that the lock file is properly created - A test to see if trying to acquire the lock twice fails --- nmrs-gui/tests/applock_test.rs | 39 ++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 nmrs-gui/tests/applock_test.rs diff --git a/nmrs-gui/tests/applock_test.rs b/nmrs-gui/tests/applock_test.rs new file mode 100644 index 00000000..c5ff080f --- /dev/null +++ b/nmrs-gui/tests/applock_test.rs @@ -0,0 +1,39 @@ +#[cfg(test)] +mod tests { + use nmrs_gui::file_lock::acquire_app_lock; + use std::sync::Mutex; + + // Used so each test can be run independently. + // Without that, with tests being run in parallel, the second test can cause the first one to fail. + static TEST_LOCK: Mutex<()> = Mutex::new(()); + + #[test] + fn test_lock_file_is_created() { + // Test if the lock file is properly created. + let _guard = TEST_LOCK.lock().unwrap(); + let file = acquire_app_lock(); + assert!(file.is_ok(), "Failed to acquire lock: {:?}", file.err()); + + let lock_dir = dirs::runtime_dir() + .or_else(dirs::data_local_dir) + .unwrap_or(std::env::temp_dir()) + .join("nmrs") + .join("app.lock"); + + assert!( + lock_dir.exists(), + "Lock file was not created at {:?}", + lock_dir + ); + } + + #[test] + fn test_second_instance_is_rejected() { + // Test if acquire_app_lock fails as it should when called twice. + let _guard = TEST_LOCK.lock().unwrap(); + let _first = acquire_app_lock().expect("First lock should succeed"); + let second = acquire_app_lock(); + assert!(second.is_err(), "Second instance should have been rejected"); + assert_eq!(second.unwrap_err(), "Another instance is already running"); + } +} From e9a4ac4e4ad5bd44a8e7e9ea5744ad18792091ab Mon Sep 17 00:00:00 2001 From: Tristan Muscat Date: Fri, 27 Mar 2026 15:18:01 +0100 Subject: [PATCH 4/4] test: move tests to file_lock.rs --- nmrs-gui/src/file_lock.rs | 38 +++++++++++++++++++++++++++++++++ nmrs-gui/tests/applock_test.rs | 39 ---------------------------------- 2 files changed, 38 insertions(+), 39 deletions(-) delete mode 100644 nmrs-gui/tests/applock_test.rs diff --git a/nmrs-gui/src/file_lock.rs b/nmrs-gui/src/file_lock.rs index c6c54fa2..5e54693e 100644 --- a/nmrs-gui/src/file_lock.rs +++ b/nmrs-gui/src/file_lock.rs @@ -22,3 +22,41 @@ pub fn acquire_app_lock() -> Result { Ok(file) } + +// Used so each test can be run independently. +// Without that, with tests being run in parallel, the second test can cause the first one to fail. +#[cfg(test)] +use std::sync::Mutex; + +#[cfg(test)] +static TEST_LOCK: Mutex<()> = Mutex::new(()); + +#[test] +fn test_lock_file_is_created() { + // Test if the lock file is properly created. + let _guard = TEST_LOCK.lock().unwrap(); + let file = acquire_app_lock(); + assert!(file.is_ok(), "Failed to acquire lock: {:?}", file.err()); + + let lock_path = dirs::runtime_dir() + .or_else(dirs::data_local_dir) + .unwrap_or(std::env::temp_dir()) + .join("nmrs") + .join("app.lock"); + + assert!( + lock_path.exists(), + "Lock file was not created at {:?}", + lock_path + ); +} + +#[test] +fn test_second_instance_is_rejected() { + // Test if acquire_app_lock fails as it should when called twice. + let _guard = TEST_LOCK.lock().unwrap(); + let _first = acquire_app_lock().expect("First lock should succeed"); + let second = acquire_app_lock(); + assert!(second.is_err(), "Second instance should have been rejected"); + assert_eq!(second.unwrap_err(), "Another instance is already running"); +} diff --git a/nmrs-gui/tests/applock_test.rs b/nmrs-gui/tests/applock_test.rs deleted file mode 100644 index c5ff080f..00000000 --- a/nmrs-gui/tests/applock_test.rs +++ /dev/null @@ -1,39 +0,0 @@ -#[cfg(test)] -mod tests { - use nmrs_gui::file_lock::acquire_app_lock; - use std::sync::Mutex; - - // Used so each test can be run independently. - // Without that, with tests being run in parallel, the second test can cause the first one to fail. - static TEST_LOCK: Mutex<()> = Mutex::new(()); - - #[test] - fn test_lock_file_is_created() { - // Test if the lock file is properly created. - let _guard = TEST_LOCK.lock().unwrap(); - let file = acquire_app_lock(); - assert!(file.is_ok(), "Failed to acquire lock: {:?}", file.err()); - - let lock_dir = dirs::runtime_dir() - .or_else(dirs::data_local_dir) - .unwrap_or(std::env::temp_dir()) - .join("nmrs") - .join("app.lock"); - - assert!( - lock_dir.exists(), - "Lock file was not created at {:?}", - lock_dir - ); - } - - #[test] - fn test_second_instance_is_rejected() { - // Test if acquire_app_lock fails as it should when called twice. - let _guard = TEST_LOCK.lock().unwrap(); - let _first = acquire_app_lock().expect("First lock should succeed"); - let second = acquire_app_lock(); - assert!(second.is_err(), "Second instance should have been rejected"); - assert_eq!(second.unwrap_err(), "Another instance is already running"); - } -}