//! PID-based lock file for toggle-on/toggle-off behavior. //! //! The first invocation creates a lock file and runs the overlay. //! A second invocation sees the lock, sends SIGUSR1 to the running //! instance, and exits immediately. use std::fs; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use anyhow::{Context, Result}; use nix::sys::signal::{self, SaFlags, SigAction, SigHandler, SigSet, Signal}; use nix::unistd::Pid; static SHOULD_EXIT: AtomicBool = AtomicBool::new(false); extern "C" fn handle_sigusr1(_: nix::libc::c_int) { SHOULD_EXIT.store(true, Ordering::Relaxed); } pub enum ToggleResult { RunNew, SignaledExisting, } /// Check if another instance is running and signal it, or indicate we should run. pub fn try_toggle() -> Result { let path = lock_file_path(); let contents = match fs::read_to_string(&path) { Ok(c) => c, Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(ToggleResult::RunNew), Err(e) => return Err(e).context("read lock file"), }; let pid: i32 = contents .trim() .parse() .context("parse PID from lock file")?; let pid = Pid::from_raw(pid); // Check if the process is alive. match signal::kill(pid, None) { Ok(()) => {} Err(nix::errno::Errno::ESRCH) => { // Stale lock — process is gone. let _ = fs::remove_file(&path); return Ok(ToggleResult::RunNew); } Err(e) => return Err(e).context("check if PID is alive"), } // Process is alive — send SIGUSR1 to tell it to exit. match signal::kill(pid, Signal::SIGUSR1) { Ok(()) => Ok(ToggleResult::SignaledExisting), Err(nix::errno::Errno::ESRCH) => { // Raced — process died between check and signal. let _ = fs::remove_file(&path); Ok(ToggleResult::RunNew) } Err(e) => Err(e).context("send SIGUSR1"), } } /// Returns true if SIGUSR1 has been received. pub fn should_exit() -> bool { SHOULD_EXIT.load(Ordering::Relaxed) } /// Create the lock file and install the SIGUSR1 handler. Returns a guard /// that removes the lock file on drop. pub fn create_lock() -> Result { let path = lock_file_path(); // Install SIGUSR1 handler before writing the lock file. // No SA_RESTART — let the signal interrupt poll() so the event loop // notices should_exit() promptly instead of waiting for the timeout. let action = SigAction::new( SigHandler::Handler(handle_sigusr1), SaFlags::empty(), SigSet::empty(), ); unsafe { signal::sigaction(Signal::SIGUSR1, &action) }.context("install SIGUSR1 handler")?; let pid = std::process::id(); fs::write(&path, pid.to_string()).context("write lock file")?; Ok(LockGuard { path }) } pub struct LockGuard { path: PathBuf, } impl Drop for LockGuard { fn drop(&mut self) { let _ = fs::remove_file(&self.path); } } fn lock_file_path() -> PathBuf { if let Ok(dir) = std::env::var("XDG_RUNTIME_DIR") { PathBuf::from(dir).join("ringa.lock") } else { PathBuf::from("/tmp/ringa.lock") } }