From 5bf73be0d670b8787cc58e15da72e5c12533ee67 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Thu, 22 Aug 2024 16:48:04 +0200 Subject: [PATCH 1/5] =?UTF-8?q?WIP:=20=E2=9C=A8=20Restrict=20running=20ses?= =?UTF-8?q?sions=20to=20one=20concurrent=20one?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This restricts the sessions that are allowed in parallel to one.TODO: Find a good way to drop the lock on destruction. --- client/src/lock.rs | 66 ++++++++++++++++++++++++++++++++++++++++++++++ client/src/main.rs | 2 ++ 2 files changed, 68 insertions(+) create mode 100644 client/src/lock.rs diff --git a/client/src/lock.rs b/client/src/lock.rs new file mode 100644 index 0000000..85434cf --- /dev/null +++ b/client/src/lock.rs @@ -0,0 +1,66 @@ +use std::env; +use std::fs::File; +use std::path::{Path, PathBuf}; + +pub const LOCK_FILE_NAME: &str = "centerpiece.lock"; +pub const XDG_RUNTIME_DIR_ENV: &str = "XDG_RUNTIME_DIR"; + +/// Queries the environment for the $XDG_RUNTIME_DIR +fn get_xdg_runtime_dir() -> Option { + env::var(XDG_RUNTIME_DIR_ENV).ok() +} + +#[derive(Debug)] +pub struct LockFile(PathBuf); + +impl LockFile { + fn init() -> Option { + Some(Self(Self::get_lock_file_path()?)) + } + + fn path(&self) -> &Path { + self.0.as_path() + } + + fn get_lock_file_path() -> Option { + let xdg_runtime_dir = get_xdg_runtime_dir()?; + Some(Path::new(&xdg_runtime_dir).join(LOCK_FILE_NAME)) + } + + fn try_lock(&self) -> std::io::Result<()> { + if self.path().is_file() { + return Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "file not found", + )); + } else { + let _ = File::create(self.path())?; + }; + Ok(()) + } + + pub fn unlock(&self) -> std::io::Result<()> { + std::fs::remove_file(&self.path())?; + Ok(()) + } + + /// Attempts to hold an exclusive lock in the runtime dir. + /// If we can't find the XDG_RUNTIME_DIR, we don't hold a lock. + /// If the lock is not successful, then exit `centerpiece`. + pub fn run_exclusive() { + if let Some(lock_file) = Self::init() { + if lock_file.try_lock().is_err() { + eprintln!( + "Could not hold an exclusive lock in {lock_file:?} stopping centerpiece." + ); + std::process::exit(1); + } + } + } +} + +impl Drop for LockFile { + fn drop(&mut self) { + let _ = self.unlock(); + } +} diff --git a/client/src/main.rs b/client/src/main.rs index 341030d..6059f1d 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -3,6 +3,7 @@ use iced::Application; mod cli; mod component; +mod lock; mod model; mod plugin; mod settings; @@ -40,6 +41,7 @@ impl Application for Centerpiece { type Flags = crate::cli::CliArgs; fn new(flags: crate::cli::CliArgs) -> (Self, iced::Command) { + let _lock = lock::LockFile::run_exclusive(); let settings = crate::settings::Settings::try_from(flags).unwrap_or_else(|_| { eprintln!("There is an issue with the settings, please check the configuration file."); std::process::exit(0); From c3818061f3b9579c0e35c11d177287991d45b522 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Fri, 23 Aug 2024 14:23:43 +0200 Subject: [PATCH 2/5] Add static reference to the lockfile --- client/src/lock.rs | 23 ++++++++++++----------- client/src/main.rs | 8 ++++++-- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/client/src/lock.rs b/client/src/lock.rs index 85434cf..fd9fbae 100644 --- a/client/src/lock.rs +++ b/client/src/lock.rs @@ -1,6 +1,7 @@ use std::env; use std::fs::File; use std::path::{Path, PathBuf}; +use std::sync::OnceLock; pub const LOCK_FILE_NAME: &str = "centerpiece.lock"; pub const XDG_RUNTIME_DIR_ENV: &str = "XDG_RUNTIME_DIR"; @@ -14,6 +15,11 @@ fn get_xdg_runtime_dir() -> Option { pub struct LockFile(PathBuf); impl LockFile { + pub fn get_or_init() -> &'static Option { + static LOCK_FILE: OnceLock> = OnceLock::new(); + LOCK_FILE.get_or_init(Self::init) + } + fn init() -> Option { Some(Self(Self::get_lock_file_path()?)) } @@ -31,7 +37,7 @@ impl LockFile { if self.path().is_file() { return Err(std::io::Error::new( std::io::ErrorKind::NotFound, - "file not found", + "File found", )); } else { let _ = File::create(self.path())?; @@ -39,16 +45,17 @@ impl LockFile { Ok(()) } - pub fn unlock(&self) -> std::io::Result<()> { - std::fs::remove_file(&self.path())?; + pub fn unlock() -> std::io::Result<()> { + if let Some(lock_file) = Self::get_or_init() { + std::fs::remove_file(&lock_file.path())?; + } Ok(()) } - /// Attempts to hold an exclusive lock in the runtime dir. /// If we can't find the XDG_RUNTIME_DIR, we don't hold a lock. /// If the lock is not successful, then exit `centerpiece`. pub fn run_exclusive() { - if let Some(lock_file) = Self::init() { + if let Some(lock_file) = Self::get_or_init() { if lock_file.try_lock().is_err() { eprintln!( "Could not hold an exclusive lock in {lock_file:?} stopping centerpiece." @@ -58,9 +65,3 @@ impl LockFile { } } } - -impl Drop for LockFile { - fn drop(&mut self) { - let _ = self.unlock(); - } -} diff --git a/client/src/main.rs b/client/src/main.rs index 6059f1d..84f9fd9 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -41,7 +41,7 @@ impl Application for Centerpiece { type Flags = crate::cli::CliArgs; fn new(flags: crate::cli::CliArgs) -> (Self, iced::Command) { - let _lock = lock::LockFile::run_exclusive(); + lock::LockFile::run_exclusive(); let settings = crate::settings::Settings::try_from(flags).unwrap_or_else(|_| { eprintln!("There is an issue with the settings, please check the configuration file."); std::process::exit(0); @@ -107,6 +107,7 @@ impl Application for Centerpiece { } iced::keyboard::Event::KeyReleased { key, .. } => { if key == iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape) { + let _ = lock::LockFile::unlock(); return iced::window::close(iced::window::Id::MAIN); } iced::Command::none() @@ -128,7 +129,10 @@ impl Application for Centerpiece { Message::UpdateEntries(plugin_id, entries) => self.update_entries(plugin_id, entries), - Message::Exit => iced::window::close(iced::window::Id::MAIN), + Message::Exit => { + let _ = lock::LockFile::unlock(); + iced::window::close(iced::window::Id::MAIN) + } } } From f342714b727f965e2ead371bf2dedd430417c6d4 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Mon, 26 Aug 2024 16:59:15 +0200 Subject: [PATCH 3/5] Implement error handling --- client/src/lock.rs | 81 +++++++++++++++++++++++++++++++++++++--------- client/src/main.rs | 4 +-- 2 files changed, 67 insertions(+), 18 deletions(-) diff --git a/client/src/lock.rs b/client/src/lock.rs index fd9fbae..bafe86e 100644 --- a/client/src/lock.rs +++ b/client/src/lock.rs @@ -1,10 +1,50 @@ use std::env; +use std::fmt; use std::fs::File; use std::path::{Path, PathBuf}; use std::sync::OnceLock; -pub const LOCK_FILE_NAME: &str = "centerpiece.lock"; -pub const XDG_RUNTIME_DIR_ENV: &str = "XDG_RUNTIME_DIR"; +const LOCK_FILE_NAME: &str = "centerpiece.lock"; +const XDG_RUNTIME_DIR_ENV: &str = "XDG_RUNTIME_DIR"; + +#[derive(Debug)] +pub enum LockFileError { + AlreadyLocked(PathBuf), + IoError(std::io::Error), + UnlockError(PathBuf, std::io::Error), +} + +type LockFileResult = Result; + +impl fmt::Display for LockFileError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let display_path = |path: &PathBuf| path.to_string_lossy().to_string(); + match self { + LockFileError::AlreadyLocked(ref path) => { + write!( + f, + "Could not hold an exclusive lock on the lockfile in {}.", + display_path(path) + ) + } + LockFileError::IoError(ref err) => write!(f, "IoError: {}", err), + LockFileError::UnlockError(ref path, ref err) => { + write!( + f, + "Failed to remove the lockfile: {} - IoError: {}", + display_path(path), + err + ) + } + } + } +} + +impl From for LockFileError { + fn from(err: std::io::Error) -> Self { + LockFileError::IoError(err) + } +} /// Queries the environment for the $XDG_RUNTIME_DIR fn get_xdg_runtime_dir() -> Option { @@ -15,7 +55,7 @@ fn get_xdg_runtime_dir() -> Option { pub struct LockFile(PathBuf); impl LockFile { - pub fn get_or_init() -> &'static Option { + fn get_or_init() -> &'static Option { static LOCK_FILE: OnceLock> = OnceLock::new(); LOCK_FILE.get_or_init(Self::init) } @@ -28,40 +68,49 @@ impl LockFile { self.0.as_path() } + fn path_buf(&self) -> PathBuf { + self.0.as_path().to_path_buf() + } + fn get_lock_file_path() -> Option { let xdg_runtime_dir = get_xdg_runtime_dir()?; Some(Path::new(&xdg_runtime_dir).join(LOCK_FILE_NAME)) } - fn try_lock(&self) -> std::io::Result<()> { + fn try_lock(&self) -> LockFileResult<()> { if self.path().is_file() { - return Err(std::io::Error::new( - std::io::ErrorKind::NotFound, - "File found", - )); + return Err(LockFileError::AlreadyLocked(self.path_buf())); } else { let _ = File::create(self.path())?; }; Ok(()) } - pub fn unlock() -> std::io::Result<()> { + fn remove_lockfile() -> LockFileResult<()> { if let Some(lock_file) = Self::get_or_init() { - std::fs::remove_file(&lock_file.path())?; + std::fs::remove_file(lock_file.path()) + .map_err(|err| LockFileError::UnlockError(lock_file.path_buf(), err))? } Ok(()) } + + /// Attempts to remove the lock file - logs errors. + /// *MUST* be run on every shutdown of `centerpiece`. + pub fn unlock() { + if let Err(err) = Self::remove_lockfile() { + log::error!("{err}"); + } + } /// Attempts to hold an exclusive lock in the runtime dir. /// If we can't find the XDG_RUNTIME_DIR, we don't hold a lock. /// If the lock is not successful, then exit `centerpiece`. pub fn run_exclusive() { - if let Some(lock_file) = Self::get_or_init() { - if lock_file.try_lock().is_err() { - eprintln!( - "Could not hold an exclusive lock in {lock_file:?} stopping centerpiece." - ); + Self::get_or_init().as_ref().map(|lock_file| { + if let Err(err) = lock_file.try_lock() { + log::error!("{err}"); + log::error!("Stopping centerpiece."); std::process::exit(1); } - } + }); } } diff --git a/client/src/main.rs b/client/src/main.rs index 84f9fd9..61dedb0 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -107,7 +107,7 @@ impl Application for Centerpiece { } iced::keyboard::Event::KeyReleased { key, .. } => { if key == iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape) { - let _ = lock::LockFile::unlock(); + lock::LockFile::unlock(); return iced::window::close(iced::window::Id::MAIN); } iced::Command::none() @@ -130,7 +130,7 @@ impl Application for Centerpiece { Message::UpdateEntries(plugin_id, entries) => self.update_entries(plugin_id, entries), Message::Exit => { - let _ = lock::LockFile::unlock(); + lock::LockFile::unlock(); iced::window::close(iced::window::Id::MAIN) } } From 8c0e6c48949c7d026bd101e98b1ae20d577854f8 Mon Sep 17 00:00:00 2001 From: a-kenji Date: Mon, 26 Aug 2024 19:57:04 +0200 Subject: [PATCH 4/5] Improve error message --- client/src/lock.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/lock.rs b/client/src/lock.rs index bafe86e..6a0d32a 100644 --- a/client/src/lock.rs +++ b/client/src/lock.rs @@ -23,7 +23,8 @@ impl fmt::Display for LockFileError { LockFileError::AlreadyLocked(ref path) => { write!( f, - "Could not hold an exclusive lock on the lockfile in {}.", + "Could not hold an exclusive lock on the lockfile in {}\n\ + If you think this is an error, please remove the lockfile manually", display_path(path) ) } From 52f658ed6d5ebf1adbad9e3011ff6b4b7dae707e Mon Sep 17 00:00:00 2001 From: a-kenji Date: Mon, 26 Aug 2024 20:12:40 +0200 Subject: [PATCH 5/5] unlock: add when settings deserialization fails --- client/src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/main.rs b/client/src/main.rs index 61dedb0..88372c4 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -11,6 +11,7 @@ mod settings; pub fn main() -> iced::Result { let args = crate::cli::CliArgs::parse(); simple_logger::init_with_level(log::Level::Info).unwrap(); + lock::LockFile::run_exclusive(); Centerpiece::run(Centerpiece::settings(args)) } @@ -41,9 +42,9 @@ impl Application for Centerpiece { type Flags = crate::cli::CliArgs; fn new(flags: crate::cli::CliArgs) -> (Self, iced::Command) { - lock::LockFile::run_exclusive(); let settings = crate::settings::Settings::try_from(flags).unwrap_or_else(|_| { eprintln!("There is an issue with the settings, please check the configuration file."); + lock::LockFile::unlock(); std::process::exit(0); });