instance.rs
raw
//! Single-instance guard.
//!
//! liftoff is a transient overlay: pressing the launch key again while it is
//! already open should do nothing, not stack a second copy. We enforce that with
//! an advisory file lock ([`flock`]) on a lock file in the user's runtime
//! directory. Taking the lock is a single non-blocking syscall, and the kernel
//! releases it automatically when the process exits — even on a crash or kill —
//! so there is no stale-lock state to detect or clean up.
//!
//! [`flock`]: https://man7.org/linux/man-pages/man2/flock.2.html
use std::fs::{File, OpenOptions};
use std::io;
use std::os::fd::AsRawFd;
use std::path::{Path, PathBuf};
/// A held single-instance lock. While this value is alive the process owns the
/// lock; dropping it (or the process exiting) releases the underlying `flock`.
pub struct InstanceLock {
// The lock is bound to this open file description and lasts exactly as long
// as the descriptor stays open. We never read or write the file; holding the
// handle is the whole point, so the field is intentionally unused.
_file: File,
}
impl InstanceLock {
/// Try to become the sole running instance by taking an exclusive,
/// non-blocking lock on `path`.
///
/// Returns `Ok(Some(lock))` when we acquired it, `Ok(None)` when another
/// instance already holds it, and `Err` only if the lock file itself could
/// not be opened.
pub fn acquire(path: &Path) -> io::Result<Option<Self>> {
let file = OpenOptions::new().create(true).write(true).open(path)?;
// LOCK_EX takes an exclusive lock; LOCK_NB makes the call fail fast with
// EWOULDBLOCK instead of blocking when another instance holds it.
let rc = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
if rc == 0 {
return Ok(Some(Self { _file: file }));
}
let err = io::Error::last_os_error();
match err.raw_os_error() {
Some(libc::EWOULDBLOCK) => Ok(None),
_ => Err(err),
}
}
}
/// The default lock-file location: `liftoff.lock` inside `$XDG_RUNTIME_DIR`.
///
/// That directory is per-user, session-scoped, and typically on tmpfs — exactly
/// where a transient runtime lock belongs. If it is unset (which should not
/// happen inside a Wayland session, since the display socket lives there too) we
/// fall back to the system temp directory with a uid-qualified name so instances
/// of different users cannot collide on a shared `/tmp`.
pub fn default_lock_path() -> PathBuf {
if let Some(dir) = std::env::var_os("XDG_RUNTIME_DIR") {
return PathBuf::from(dir).join("liftoff.lock");
}
let uid = unsafe { libc::getuid() };
std::env::temp_dir().join(format!("liftoff-{uid}.lock"))
}
#[cfg(test)]
mod tests {
use super::*;
/// A second acquisition of the same lock file, while the first is still
/// held, reports that another instance owns it. flock locks are per open
/// file description, so two separate opens contend even within one process.
#[test]
fn second_acquire_is_blocked_while_first_is_held() {
let path = std::env::temp_dir()
.join(format!("liftoff-test-blocked-{}.lock", std::process::id()));
let first = InstanceLock::acquire(&path)
.expect("opening the lock file should succeed")
.expect("the first instance should acquire the lock");
let second = InstanceLock::acquire(&path).expect("opening the lock file should succeed");
assert!(
second.is_none(),
"a second instance must not acquire the lock while the first holds it"
);
drop(first);
let _ = std::fs::remove_file(&path);
}
/// Once the holder is dropped the lock is released, so a fresh acquisition
/// succeeds again.
#[test]
fn acquire_succeeds_again_after_release() {
let path = std::env::temp_dir()
.join(format!("liftoff-test-release-{}.lock", std::process::id()));
let first = InstanceLock::acquire(&path)
.expect("opening the lock file should succeed")
.expect("the first instance should acquire the lock");
drop(first);
let second = InstanceLock::acquire(&path)
.expect("opening the lock file should succeed")
.expect("the lock should be available again after release");
drop(second);
let _ = std::fs::remove_file(&path);
}
}