diff --git a/Cargo.lock b/Cargo.lock index 3e8e290..6b943ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -512,6 +512,7 @@ dependencies = [ "dbus", "eframe", "freedesktop-desktop-entry", + "libc", "log", "networkmanager", "nucleo-matcher", diff --git a/client/Cargo.toml b/client/Cargo.toml index 79fa674..11823f0 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -11,6 +11,7 @@ edition.workspace = true # general settings = { path = "../settings" } anyhow = { version = "1.0.100", features = ["backtrace"] } +libc = "0.2" clap = { version = "4.5.21", features = ["derive", "env"] } log = { version = "0.4.22", features = ["kv_unstable_serde"] } simple_logger = { version = "5.0.0", features = [ diff --git a/client/src/lock.rs b/client/src/lock.rs new file mode 100644 index 0000000..8e00d46 --- /dev/null +++ b/client/src/lock.rs @@ -0,0 +1,89 @@ +use std::env; +use std::fs::{File, TryLockError}; +use std::io::{Read, Seek, Write}; +use std::path::{Path, PathBuf}; + +const LOCK_FILE_NAME: &str = "centerpiece.lock"; + +pub struct LockFile { + _file: File, +} + +impl LockFile { + pub fn acquire() -> Option { + let path = lock_file_path()?; + let mut file = open_lock_file(&path)?; + + match file.try_lock() { + Ok(()) => { + write_pid(&mut file); + log::info!("Acquired exclusive lock"); + Some(Self { _file: file }) + } + Err(TryLockError::WouldBlock) => { + kill_existing_instance(&mut file); + // Blocks until the old process exits and releases the lock + file.lock().ok()?; + write_pid(&mut file); + log::info!("Acquired exclusive lock after killing previous instance"); + Some(Self { _file: file }) + } + Err(TryLockError::Error(e)) => { + log::error!("Failed to acquire lock: {e}"); + None + } + } + } + + pub fn cleanup() { + if let Some(path) = lock_file_path() { + let _ = std::fs::remove_file(path); + } + } +} + +fn lock_file_path() -> Option { + let dir = env::var("XDG_RUNTIME_DIR").ok(); + if dir.is_none() { + log::warn!("XDG_RUNTIME_DIR not set, running without file lock"); + } + Some(Path::new(&dir?).join(LOCK_FILE_NAME)) +} + +fn open_lock_file(path: &Path) -> Option { + File::options() + .read(true) + .write(true) + .create(true) + .truncate(false) + .open(path) + .map_err(|e| log::error!("Failed to open lock file: {e}")) + .ok() +} + +fn write_pid(file: &mut File) { + let _ = file.set_len(0); + let _ = file.seek(std::io::SeekFrom::Start(0)); + let _ = write!(file, "{}", std::process::id()); + let _ = file.flush(); +} + +fn read_pid(file: &mut File) -> Option { + let mut contents = String::new(); + file.seek(std::io::SeekFrom::Start(0)).ok()?; + file.read_to_string(&mut contents).ok()?; + contents.trim().parse().ok() +} + +fn kill_existing_instance(file: &mut File) { + let Some(pid) = read_pid(file) else { + log::warn!("Lock held but no PID in lock file"); + return; + }; + log::info!("Killing existing instance with PID {pid}"); + // SAFETY: sending SIGTERM to a known PID + unsafe { + libc::kill(pid as i32, libc::SIGTERM); + } +} + diff --git a/client/src/main.rs b/client/src/main.rs index 11f85c9..f49999e 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::acquire(); + eframe::run_native( "centerpiece", settings(), @@ -300,6 +301,11 @@ impl Centerpiece { } } + fn exit() -> ! { + lock::LockFile::cleanup(); + 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(); + } } } }