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.
108 lines
3.2 KiB
Rust
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")
|
|
}
|
|
}
|