#![deny(unsafe_code)]
use std::{
env,
ffi::OsString,
os::{
fd::{AsRawFd, RawFd},
unix::ffi::OsStrExt,
},
process::{Command, ExitCode},
};
use btoi::btoi;
use nix::{
errno::Errno,
fcntl::{OFlag, AT_FDCWD},
sys::{
signal::{sigaction, SaFlags, SigAction, SigHandler, Signal},
signalfd::SigSet,
stat::Mode,
},
};
use syd::{
compat::{dup3, openat2, OpenHow, ResolveFlag},
config::{ENV_SH, SYD_SH},
confine::run_cmd,
fd::set_cloexec,
ofd::lock_fd,
path::XPathBuf,
retry::retry_on_eintr,
timer::AlarmTimer,
};
#[cfg(all(
not(coverage),
not(feature = "prof"),
not(target_os = "android"),
not(target_arch = "riscv64"),
target_page_size_4k,
target_pointer_width = "64"
))]
#[global_allocator]
static GLOBAL: hardened_malloc::HardenedMalloc = hardened_malloc::HardenedMalloc;
#[cfg(feature = "prof")]
#[global_allocator]
static GLOBAL: tcmalloc::TCMalloc = tcmalloc::TCMalloc;
extern "C" fn handle_sigalrm(_: libc::c_int) {}
syd::main! {
use lexopt::prelude::*;
syd::set_sigpipe_dfl()?;
let mut opt_block = true;
let mut opt_fdset = None;
let mut opt_tmout = None;
let mut opt_plock = None;
let mut opt_wlock = true;
let mut opt_cmd = env::var_os(ENV_SH).unwrap_or(OsString::from(SYD_SH));
let mut opt_arg = Vec::new();
let mut parser = lexopt::Parser::from_env();
while let Some(arg) = parser.next()? {
match arg {
Short('h') => {
help();
return Ok(ExitCode::SUCCESS);
}
Short('n') => opt_block = false,
Short('N') => opt_block = true,
Short('r' | 's') => opt_wlock = false,
Short('w' | 'x') => opt_wlock = true,
Short('d') => opt_fdset = Some(btoi::<RawFd>(parser.value()?.as_bytes())?),
Short('t') => opt_tmout = Some(btoi::<u64>(parser.value()?.as_bytes())?),
Value(lock) => {
opt_plock = Some(XPathBuf::from(lock));
let mut raw = parser.raw_args()?;
if let Some(cmd) = raw.next() {
opt_cmd = cmd;
opt_arg.extend(raw);
}
}
_ => return Err(arg.unexpected().into()),
}
}
let opt_plock = if let Some(opt_plock) = opt_plock {
opt_plock
} else {
eprintln!("syd-ofd: Lock path is required!");
return Err(Errno::ENOENT.into());
};
if opt_plock.has_parent_dot() {
eprintln!("syd-ofd: Parent directory (..) components aren't permitted in lock path!");
return Err(Errno::EACCES.into());
}
let mode = Mode::from_bits_truncate(0o600);
let mut flags = OFlag::O_CREAT | OFlag::O_CLOEXEC | OFlag::O_NONBLOCK | OFlag::O_NOCTTY | OFlag::O_NOFOLLOW;
if opt_wlock {
flags.insert(OFlag::O_WRONLY);
} else {
flags.insert(OFlag::O_RDONLY);
}
let how = OpenHow::new()
.flags(flags)
.mode(mode)
.resolve(ResolveFlag::RESOLVE_NO_MAGICLINKS | ResolveFlag::RESOLVE_NO_SYMLINKS);
#[expect(clippy::disallowed_methods)]
let mut fd = retry_on_eintr(|| openat2(AT_FDCWD, &opt_plock, how))?;
let timer = if let Some(tmout) = opt_tmout {
opt_block = true;
let sig_action = SigAction::new(
SigHandler::Handler(handle_sigalrm),
SaFlags::empty(),
SigSet::empty(),
);
#[expect(unsafe_code)]
unsafe { sigaction(Signal::SIGALRM, &sig_action) }?;
let mut timer = AlarmTimer::from_milliseconds(tmout)?;
timer.start()?;
Some(timer)
} else {
None
};
lock_fd(&fd, opt_wlock, opt_block)?;
drop(timer);
if let Some(opt_fdset) = opt_fdset {
if opt_fdset != fd.as_raw_fd() {
fd = dup3(fd.as_raw_fd(), opt_fdset, OFlag::O_CLOEXEC.bits())?;
}
}
set_cloexec(&fd, false)?;
let mut cmd = Command::new(opt_cmd);
let cmd = cmd.args(opt_arg);
Ok(ExitCode::from(run_cmd(cmd)))
}
fn help() {
println!(
"Usage: syd-ofd [-n | -N] [-t timeout] [-d fd] [-s=-r | -x=-w] file {{command [arg...]}}"
);
println!("Take a lock on a file, then execute into another program.");
println!("Use -n to take a nonblocking lock.");
println!("Use -N to take a blocking lock. This is the default.");
println!("Use -t timeout to specify a timeout in milliseconds.");
println!("Use -s or -r to take a shared lock.");
println!("Use -x or -w to take an exclusive lock. This is the default.");
println!("Use -d fd to make the lock visible to program on file descriptor fd.");
}