use std::io::Read as _;
use std::os::fd::{AsRawFd as _, RawFd};
use std::os::unix::process::CommandExt as _;
use std::process::{Child, Command};
use std::thread::ScopedJoinHandle;
use super::CallGuard;
pub enum Refusal {
NotStarted(std::io::Error),
NotLeader,
NoGuard(String),
}
pub fn start_behind_guard(command: &mut Command) -> Result<(Child, CallGuard), Refusal> {
let (gate, open) = std::io::pipe().map_err(Refusal::NotStarted)?;
let (mut announced, announce) = std::io::pipe().map_err(Refusal::NotStarted)?;
let (gate_fd, open_fd, announce_fd) =
(gate.as_raw_fd(), open.as_raw_fd(), announce.as_raw_fd());
unsafe {
command.pre_exec(move || wait_at_gate(gate_fd, open_fd, announce_fd));
}
std::thread::scope(|scope| {
let spawning = scope.spawn(move || {
let spawned = command.spawn();
drop((gate, announce));
spawned
});
let mut pid = [0_u8; size_of::<libc::pid_t>()];
if announced.read_exact(&mut pid).is_err() {
drop(open);
return Err(Refusal::NotStarted(match finish(spawning) {
Err(error) => error,
Ok(_) => std::io::Error::other("the process did not wait for its guard"),
}));
}
let pid = libc::pid_t::from_ne_bytes(pid);
#[cfg(any(test, feature = "test-support"))]
pause_before_guard(pid);
let leads = unsafe { libc::getpgid(pid) } == pid;
let guard = if leads {
attach_guard(pid).map_err(Refusal::NoGuard)
} else {
Err(Refusal::NotLeader)
};
let guard = match guard {
Ok(guard) => guard,
Err(refusal) => {
drop(open);
let _ = finish(spawning);
return Err(refusal);
}
};
let opened = super::write_fd(open.as_raw_fd(), &[1]);
drop(open);
match finish(spawning) {
Ok(child) if opened => Ok((child, guard)),
Ok(_) => Err(Refusal::NotStarted(std::io::Error::other(
"the gate could not be opened",
))),
Err(error) => Err(Refusal::NotStarted(error)),
}
})
}
fn finish(spawning: ScopedJoinHandle<'_, std::io::Result<Child>>) -> std::io::Result<Child> {
spawning
.join()
.unwrap_or_else(|_| Err(std::io::Error::other("the start did not finish")))
}
#[cfg(not(any(test, feature = "test-support")))]
fn attach_guard(process: libc::pid_t) -> Result<CallGuard, String> {
CallGuard::attach_to(process, process)
}
#[cfg(any(test, feature = "test-support"))]
fn attach_guard(process: libc::pid_t) -> Result<CallGuard, String> {
let nowhere = super::tests::NO_SUCH_GROUP;
match GUARD_FAULT.with(std::cell::Cell::get) {
None => CallGuard::attach_to(process, process),
Some(GuardFault::CannotJoin) => CallGuard::attach_to(nowhere, nowhere),
Some(GuardFault::WrongGroup) => CallGuard::attach_to(process, nowhere),
}
}
#[cfg(any(test, feature = "test-support"))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GuardFault {
CannotJoin,
WrongGroup,
}
#[cfg(any(test, feature = "test-support"))]
thread_local! {
static GUARD_FAULT: std::cell::Cell<Option<GuardFault>> = const { std::cell::Cell::new(None) };
static LAST_GATED: std::cell::Cell<libc::pid_t> = const { std::cell::Cell::new(0) };
}
#[cfg(any(test, feature = "test-support"))]
pub fn set_guard_fault_for_test(fault: Option<GuardFault>) {
GUARD_FAULT.with(|current| current.set(fault));
}
#[cfg(any(test, feature = "test-support"))]
pub(crate) fn last_gated_for_test() -> libc::pid_t {
LAST_GATED.with(std::cell::Cell::get)
}
#[cfg(any(test, feature = "test-support"))]
fn pause_before_guard(process: libc::pid_t) {
LAST_GATED.with(|last| last.set(process));
if std::env::var_os(super::tests::PAUSE_ENV).is_some() {
let _ = super::write_fd(
libc::STDOUT_FILENO,
format!("FORKED {process}\n").as_bytes(),
);
std::thread::sleep(std::time::Duration::from_mins(1));
}
}
fn wait_at_gate(gate: RawFd, open: RawFd, announce: RawFd) -> std::io::Result<()> {
unsafe {
libc::close(open);
let pid = libc::getpid().to_ne_bytes();
let told = libc::write(announce, pid.as_ptr().cast(), pid.len());
libc::close(announce);
if told != pid.len() as isize {
return Err(std::io::Error::from_raw_os_error(libc::EPIPE));
}
let mut byte = 0_u8;
loop {
match libc::read(gate, (&raw mut byte).cast(), 1) {
1 => return Ok(()),
0 => return Err(std::io::Error::from_raw_os_error(libc::ECANCELED)),
_ => {
let error = std::io::Error::last_os_error();
if error.raw_os_error() != Some(libc::EINTR) {
return Err(error);
}
}
}
}
}
}