diff --git a/client/src/lock.rs b/client/src/lock.rs new file mode 100644 index 0000000..e56b4ae --- /dev/null +++ b/client/src/lock.rs @@ -0,0 +1,140 @@ +use std::env; +use std::fmt; +use std::fs::File; +use std::fs::TryLockError; +use std::path::{Path, PathBuf}; + +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(path) => { + write!( + f, + "Another instance is already running (lock: {})\n\ + If you think this is an error, please remove the lockfile manually", + display_path(path) + ) + } + LockFileError::IoError(err) => write!(f, "IoError: {}", err), + LockFileError::UnlockError(path, 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) + } +} + +fn get_xdg_runtime_dir() -> Option { + env::var(XDG_RUNTIME_DIR_ENV).ok() +} + +#[derive(Debug)] +pub struct LockFile { + path: PathBuf, + file: Option, +} + +impl LockFile { + fn new() -> Option { + Some(Self { + path: Self::get_lock_file_path()?, + file: None, + }) + } + + fn path(&self) -> &Path { + self.path.as_ref() + } + + fn path_buf(&self) -> PathBuf { + self.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(&mut self) -> LockFileResult<()> { + let file = std::fs::File::options() + .write(true) + .create(true) + .truncate(false) + .open(self.path())?; + + match file.try_lock() { + Ok(()) => { + self.file = Some(file); + Ok(()) + } + Err(TryLockError::WouldBlock) => Err(LockFileError::AlreadyLocked(self.path_buf())), + Err(TryLockError::Error(e)) => Err(LockFileError::IoError(e)), + } + } + + fn remove_lockfile() -> LockFileResult<()> { + if let Some(lock_file) = Self::new() { + std::fs::remove_file(lock_file.path()) + .map_err(|err| LockFileError::UnlockError(lock_file.path_buf(), err))? + } + Ok(()) + } + + pub fn unlock() { + if let Err(err) = Self::remove_lockfile() { + log::error!("{err}"); + } + } + + pub fn acquire_exclusive_lock() -> LockFileResult { + match Self::new() { + Some(mut lock_file) => { + lock_file.try_lock()?; + Ok(lock_file) + } + None => { + log::warn!("XDG_RUNTIME_DIR not found, running without file lock"); + Err(LockFileError::IoError(std::io::Error::new( + std::io::ErrorKind::NotFound, + "XDG_RUNTIME_DIR environment variable not set", + ))) + } + } + } + + pub fn run_exclusive() -> Self { + match Self::acquire_exclusive_lock() { + Ok(lock_file) => { + log::info!("Successfully acquired exclusive lock"); + lock_file + } + Err(err) => { + log::warn!("{err}"); + std::process::exit(0); + } + } + } +} diff --git a/client/src/main.rs b/client/src/main.rs index 11f85c9..f1ca143 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,9 +1,8 @@ -use std::process::exit; - use clap::Parser; use eframe::egui::{self, Separator}; mod component; +mod lock; mod model; mod plugin; @@ -16,6 +15,8 @@ pub fn main() { simple_logger::init_with_level(log::Level::Info).unwrap(); + let _lock = lock::LockFile::run_exclusive(); + eframe::run_native( "centerpiece", settings(), @@ -300,6 +301,11 @@ impl Centerpiece { } } + fn exit(&self) -> ! { + lock::LockFile::unlock(); + std::process::exit(0); + } + fn handle_input(&mut self, ctx: &eframe::egui::Context) { if ctx.input(|i| i.key_pressed(eframe::egui::Key::ArrowUp)) { self.select_previous_entry(); @@ -311,7 +317,7 @@ impl Centerpiece { self.activate_selected_entry(); } if ctx.input(|i| i.key_pressed(eframe::egui::Key::Escape)) { - exit(0); + self.exit(); } if ctx.input(|i| i.modifiers.ctrl && i.key_pressed(eframe::egui::Key::J)) { @@ -337,7 +343,9 @@ impl Centerpiece { self.update_entries(plugin_id, entries) } - Message::Exit => exit(0), + Message::Exit => { + self.exit(); + } } } }