use std::io::Read as _;
use std::os::unix::process::CommandExt as _;
use std::process::{Child, ChildStdout, Command, Stdio};
use std::time::Duration;
use super::{END_BUDGET, EXIT_CONTAINMENT, EXIT_USAGE, GroupEnd};
const READY: &[u8] = b"leanctx-group-guard ready\n";
const EXIT_UNCONFIRMED: i32 = 2;
const EXIT_SURVIVORS: i32 = 3;
const READY_BUDGET: Duration = Duration::from_secs(5);
const REAP_BUDGET: Duration = Duration::from_secs(2);
pub(super) fn main(mut args: impl Iterator<Item = std::ffi::OsString>) -> i32 {
let group = match (args.next(), args.next(), args.next()) {
(Some(flag), Some(value), None) if flag == "--group" => value
.to_str()
.and_then(|value| value.parse::<libc::pid_t>().ok()),
_ => None,
};
group.filter(|group| *group > 1).map_or(EXIT_USAGE, run)
}
pub(super) fn run(expected: libc::pid_t) -> i32 {
if unsafe { libc::getpgrp() } != expected {
return EXIT_CONTAINMENT;
}
super::set_termination_signals(libc::SIG_IGN);
let pin = unsafe { libc::fork() };
if pin == 0 {
unsafe { libc::_exit(0) };
}
if pin < 0 {
return EXIT_CONTAINMENT;
}
let left = unsafe { libc::setpgid(0, 0) } == 0 && unsafe { libc::getpgrp() } != expected;
if !left {
reap_pin(pin);
return EXIT_CONTAINMENT;
}
let _ = super::write_fd(libc::STDOUT_FILENO, READY);
wait_for_end_of_input();
let end = super::end_pinned_group(expected, END_BUDGET);
reap_pin(pin);
match end {
GroupEnd::Empty => 0,
GroupEnd::Unconfirmed => EXIT_UNCONFIRMED,
GroupEnd::Survivors => EXIT_SURVIVORS,
GroupEnd::Refused => EXIT_CONTAINMENT,
}
}
fn wait_for_end_of_input() {
let mut buffer = [0_u8; 64];
loop {
let read =
unsafe { libc::read(libc::STDIN_FILENO, buffer.as_mut_ptr().cast(), buffer.len()) };
if read == 0 {
return;
}
if read < 0 && std::io::Error::last_os_error().kind() != std::io::ErrorKind::Interrupted {
return;
}
}
}
fn reap_pin(pin: libc::pid_t) {
let mut status: libc::c_int = 0;
unsafe { libc::waitpid(pin, &raw mut status, 0) };
}
fn guard_command(expected: libc::pid_t) -> std::io::Result<Command> {
#[cfg(any(test, feature = "test-support"))]
if super::harness_helper() {
return harness_guard_command(expected);
}
let mut command = Command::new(std::env::current_exe()?);
command.args([super::GUARD_MODE, "--group", &expected.to_string()]);
Ok(command)
}
#[cfg(any(test, feature = "test-support"))]
fn harness_guard_command(expected: libc::pid_t) -> std::io::Result<Command> {
let mut command = Command::new(std::env::current_exe()?);
command
.args([
super::tests::HELPER_TEST,
"--exact",
"--ignored",
"--nocapture",
])
.env(super::tests::HELPER_ENV, "group-guard")
.env(super::tests::GROUP_ENV, expected.to_string());
Ok(command)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GuardOutcome {
Ended,
Unconfirmed,
Failed,
}
pub struct CallGuard {
guard: Child,
hold: Option<std::io::PipeWriter>,
}
impl CallGuard {
pub(super) fn attach_to(join: libc::pid_t, expected: libc::pid_t) -> Result<Self, String> {
let (reader, writer) = std::io::pipe().map_err(|error| error.to_string())?;
let mut command = guard_command(expected).map_err(|error| error.to_string())?;
command
.stdin(reader)
.stdout(Stdio::piped())
.stderr(Stdio::null())
.process_group(join);
let mut guard = command
.spawn()
.map_err(|error| format!("the guard could not join the group: {error}"))?;
drop(command);
let ready = guard
.stdout
.take()
.is_some_and(|output| ready_within(output, READY_BUDGET));
if !ready {
drop(writer);
let _ = guard.kill();
let _ = crate::shell::exec::reap_within(&mut guard, REAP_BUDGET);
return Err("the guard did not become ready".to_string());
}
Ok(Self {
guard,
hold: Some(writer),
})
}
pub fn finish(mut self) -> GuardOutcome {
drop(self.hold.take());
let outcome = if let Some(status) =
crate::shell::exec::reap_within(&mut self.guard, END_BUDGET + REAP_BUDGET)
{
match status.code() {
Some(0) => GuardOutcome::Ended,
Some(EXIT_UNCONFIRMED) => GuardOutcome::Unconfirmed,
_ => GuardOutcome::Failed,
}
} else {
let _ = self.guard.kill();
let _ = crate::shell::exec::reap_within(&mut self.guard, REAP_BUDGET);
GuardOutcome::Failed
};
#[cfg(any(test, feature = "test-support"))]
if let Some(forced) = FORCED_OUTCOME.with(std::cell::Cell::get) {
return forced;
}
outcome
}
}
impl Drop for CallGuard {
fn drop(&mut self) {
if self.hold.take().is_some()
&& crate::shell::exec::reap_within(&mut self.guard, END_BUDGET + REAP_BUDGET).is_none()
{
let _ = self.guard.kill();
let _ = crate::shell::exec::reap_within(&mut self.guard, REAP_BUDGET);
}
}
}
#[cfg(any(test, feature = "test-support"))]
thread_local! {
static FORCED_OUTCOME: std::cell::Cell<Option<GuardOutcome>> =
const { std::cell::Cell::new(None) };
}
#[cfg(any(test, feature = "test-support"))]
pub fn force_outcome_for_test(outcome: Option<GuardOutcome>) {
FORCED_OUTCOME.with(|forced| forced.set(outcome));
}
fn ready_within(mut output: ChildStdout, budget: Duration) -> bool {
const MAX_PREAMBLE: usize = 8192;
let (sender, receiver) = std::sync::mpsc::channel();
std::thread::spawn(move || {
let mut seen = Vec::new();
let mut chunk = [0_u8; 256];
let found = loop {
match output.read(&mut chunk) {
Err(error) if error.kind() == std::io::ErrorKind::Interrupted => {}
Ok(0) | Err(_) => break false,
Ok(read) => {
seen.extend_from_slice(&chunk[..read]);
if seen.windows(READY.len()).any(|window| window == READY) {
break true;
}
if seen.len() > MAX_PREAMBLE {
break false;
}
}
}
};
let _ = sender.send(found);
});
receiver.recv_timeout(budget).unwrap_or(false)
}