diff --git a/client/src/lock.rs b/client/src/lock.rs new file mode 100644 index 0000000..6a0d32a --- /dev/null +++ b/client/src/lock.rs @@ -0,0 +1,117 @@ +use std::env; +use std::fmt; +use std::fs::File; +use std::path::{Path, PathBuf}; +use std::sync::OnceLock; + +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 {}\n\ + If you think this is an error, please remove the lockfile manually", + 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 { + env::var(XDG_RUNTIME_DIR_ENV).ok() +} + +#[derive(Debug)] +pub struct LockFile(PathBuf); + +impl LockFile { + 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()?)) + } + + fn path(&self) -> &Path { + 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) -> LockFileResult<()> { + if self.path().is_file() { + return Err(LockFileError::AlreadyLocked(self.path_buf())); + } else { + let _ = File::create(self.path())?; + }; + Ok(()) + } + + fn remove_lockfile() -> LockFileResult<()> { + if let Some(lock_file) = Self::get_or_init() { + 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() { + 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 341030d..88372c4 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; @@ -10,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)) } @@ -42,6 +44,7 @@ impl Application for Centerpiece { fn new(flags: crate::cli::CliArgs) -> (Self, iced::Command) { 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); }); @@ -105,6 +108,7 @@ impl Application for Centerpiece { } iced::keyboard::Event::KeyReleased { key, .. } => { if key == iced::keyboard::Key::Named(iced::keyboard::key::Named::Escape) { + lock::LockFile::unlock(); return iced::window::close(iced::window::Id::MAIN); } iced::Command::none() @@ -126,7 +130,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 => { + lock::LockFile::unlock(); + iced::window::close(iced::window::Id::MAIN) + } } }