feat: single-instance lock and detached background child
This commit is contained in:
1 parent
6e069e3e70
commit
bc3a0bab82
3 files changed
+149
No files matched your search
@@ -0,0 +1,71 @@
|
|||||||
|
use std::fs::File;
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
|
||||||
|
use crate::lock::Lock;
|
||||||
|
|
||||||
|
/// Runs `work` in a forked child that outlives valw, like `wl-copy` does.
|
||||||
|
/// The parent returns right away.
|
||||||
|
///
|
||||||
|
/// The child drops the capture lock (so the next capture isn't blocked) and
|
||||||
|
/// points stdio at /dev/null (so `valw ... -o - | x` sees EOF without waiting
|
||||||
|
/// for the clipboard to be replaced).
|
||||||
|
pub fn spawn(lock: Lock, work: impl FnOnce()) -> Result<()> {
|
||||||
|
lock.release();
|
||||||
|
// SAFETY: valw is single-threaded at this point, and the child only
|
||||||
|
// touches its own state before exiting with _exit.
|
||||||
|
match unsafe { libc::fork() } {
|
||||||
|
-1 => Err(io::Error::last_os_error()).context("could not fork the clipboard server"),
|
||||||
|
0 => {
|
||||||
|
let _ = rustix::process::setsid();
|
||||||
|
let _ = detach_stdio();
|
||||||
|
work();
|
||||||
|
// SAFETY: skips atexit handlers and destructors that belong to the parent.
|
||||||
|
unsafe { libc::_exit(0) }
|
||||||
|
}
|
||||||
|
_ => Ok(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn detach_stdio() -> io::Result<()> {
|
||||||
|
let null = File::options().read(true).write(true).open("/dev/null")?;
|
||||||
|
rustix::stdio::dup2_stdin(&null)?;
|
||||||
|
rustix::stdio::dup2_stdout(&null)?;
|
||||||
|
rustix::stdio::dup2_stderr(&null)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn child_holds_neither_lock_nor_stdio() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("valw.lock");
|
||||||
|
let lock = Lock::acquire(&path).unwrap();
|
||||||
|
let (report_r, report_w) = rustix::pipe::pipe().unwrap();
|
||||||
|
let (go_r, go_w) = rustix::pipe::pipe().unwrap();
|
||||||
|
|
||||||
|
spawn(lock, move || {
|
||||||
|
// Report whether stdout is /dev/null, then wait for the parent.
|
||||||
|
let null = rustix::fs::stat("/dev/null").unwrap();
|
||||||
|
let out = rustix::fs::fstat(rustix::stdio::stdout()).unwrap();
|
||||||
|
let is_null = out.st_rdev == null.st_rdev && out.st_ino == null.st_ino;
|
||||||
|
rustix::io::write(&report_w, &[is_null as u8]).unwrap();
|
||||||
|
let _ = rustix::io::read(&go_r, &mut [0u8; 1]);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// The child is still running, yet the lock is free.
|
||||||
|
Lock::acquire(&path).unwrap();
|
||||||
|
|
||||||
|
let mut is_null = [0u8; 1];
|
||||||
|
rustix::io::read(&report_r, &mut is_null).unwrap();
|
||||||
|
assert_eq!(is_null, [1], "child stdout should be /dev/null");
|
||||||
|
|
||||||
|
// Closing our end lets the child exit.
|
||||||
|
drop(go_w);
|
||||||
|
}
|
||||||
|
}
|
||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
use std::fs::File;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use anyhow::{Context, Result, bail};
|
||||||
|
use rustix::fs::{FlockOperation, flock};
|
||||||
|
use rustix::io::Errno;
|
||||||
|
|
||||||
|
use crate::error::HintExt;
|
||||||
|
|
||||||
|
/// Held for the whole capture so two captures never overlap. The kernel
|
||||||
|
/// drops the lock when the process dies, so a crash can't leave it stuck.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Lock {
|
||||||
|
_file: File,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `$XDG_RUNTIME_DIR/valw.lock`, falling back to the temp dir.
|
||||||
|
pub fn default_path() -> PathBuf {
|
||||||
|
std::env::var_os("XDG_RUNTIME_DIR")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(std::env::temp_dir)
|
||||||
|
.join("valw.lock")
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Lock {
|
||||||
|
pub fn acquire(path: &Path) -> Result<Lock> {
|
||||||
|
let file = File::options()
|
||||||
|
.create(true)
|
||||||
|
.truncate(false)
|
||||||
|
.write(true)
|
||||||
|
.open(path)
|
||||||
|
.with_context(|| format!("could not open lock file {}", path.display()))?;
|
||||||
|
match flock(&file, FlockOperation::NonBlockingLockExclusive) {
|
||||||
|
Ok(()) => Ok(Lock { _file: file }),
|
||||||
|
Err(Errno::WOULDBLOCK) => Err(anyhow::anyhow!("valw is already running"))
|
||||||
|
.hint("wait for the other capture to finish, or press Esc in it"),
|
||||||
|
Err(e) => {
|
||||||
|
bail!("could not lock {}: {e}", path.display())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Releases the lock. Must happen before forking, or the child would
|
||||||
|
/// inherit the file and keep the lock held.
|
||||||
|
pub fn release(self) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn second_acquire_fails_until_release() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("valw.lock");
|
||||||
|
|
||||||
|
let first = Lock::acquire(&path).unwrap();
|
||||||
|
let err = Lock::acquire(&path).unwrap_err();
|
||||||
|
assert_eq!(
|
||||||
|
err.to_string(),
|
||||||
|
"wait for the other capture to finish, or press Esc in it"
|
||||||
|
);
|
||||||
|
assert_eq!(err.root_cause().to_string(), "valw is already running");
|
||||||
|
|
||||||
|
first.release();
|
||||||
|
Lock::acquire(&path).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stale_lock_file_is_fine() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("valw.lock");
|
||||||
|
File::create(&path).unwrap();
|
||||||
|
Lock::acquire(&path).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
|
mod detach;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod lock;
|
||||||
mod log;
|
mod log;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|||||||
Reference in new issue
Block a user