Files
ringa/src/lock.rs
T
swaits 421bd04339 fix: instant toggle exit via SIGUSR1 poll interruption
Remove SA_RESTART from the signal handler so SIGUSR1 interrupts the
blocking poll() call with EINTR. Handle EINTR gracefully in the event
loop to check should_exit() immediately instead of waiting for timeout.
2026-04-07 12:28:42 -06:00

108 lines
3.2 KiB
Rust

//! 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<ToggleResult> {
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<LockGuard> {
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")
}
}