use std::ffi::OsString;
use std::os::fd::{OwnedFd, RawFd};
use std::os::unix::process::{CommandExt as _, ExitStatusExt as _};
use std::process::{Child, Command, Stdio};
use std::sync::atomic::{AtomicI32, Ordering};
use super::wire::{Inbox, Reply, Request, SpawnSpec, send_frame};
use super::{END_BUDGET, EXIT_CONTAINMENT, EXIT_USAGE};
use crate::shell::exec::exited_unreaped;
pub(super) const HELLO: &[u8] = b"leanctx-spawn-helper 1";
static CHILD_EVENT: AtomicI32 = AtomicI32::new(-1);
extern "C" fn child_changed(_signal: libc::c_int) {
let descriptor = CHILD_EVENT.load(Ordering::Relaxed);
if descriptor >= 0 {
unsafe { libc::write(descriptor, [1_u8].as_ptr().cast(), 1) };
}
}
pub(crate) fn lead_new_session(command: &mut Command) {
unsafe {
command.pre_exec(|| {
if libc::setsid() == -1 {
return Err(std::io::Error::last_os_error());
}
Ok(())
});
}
}
pub(super) fn main(mut args: impl Iterator<Item = OsString>) -> i32 {
if args.next().is_some() {
return EXIT_USAGE;
}
serve(libc::STDIN_FILENO)
}
struct Held {
call: u64,
child: Child,
reported: bool,
killed: bool,
released: bool,
}
pub(super) fn serve(socket: RawFd) -> i32 {
unsafe { libc::setsid() };
let Some(wake) = watch_children() else {
return EXIT_CONTAINMENT;
};
super::wire::quiet_socket(socket);
if send_frame(socket, HELLO, &[]).is_err() {
return 0;
}
let mut inbox = Inbox::default();
let mut held: Vec<Held> = Vec::new();
let orderly = loop {
if !report_exits(socket, &mut held) {
break false;
}
if !wait_for_event(socket, wake) {
continue;
}
if !matches!(inbox.fill(socket), Ok(true)) {
break false;
}
match handle_requests(socket, &mut inbox, &mut held) {
Some(true) => break true,
Some(false) => break false,
None => {}
}
};
if !orderly {
end_held(&mut held);
}
0
}
fn watch_children() -> Option<RawFd> {
let mut ends: [RawFd; 2] = [0; 2];
unsafe {
if libc::pipe(ends.as_mut_ptr()) != 0 {
return None;
}
for end in ends {
libc::fcntl(end, libc::F_SETFD, libc::FD_CLOEXEC);
libc::fcntl(end, libc::F_SETFL, libc::O_NONBLOCK);
}
CHILD_EVENT.store(ends[1], Ordering::Relaxed);
libc::signal(
libc::SIGCHLD,
child_changed as extern "C" fn(libc::c_int) as libc::sighandler_t,
);
for signal in [libc::SIGTERM, libc::SIGINT, libc::SIGHUP] {
let before = libc::signal(
signal,
stay as extern "C" fn(libc::c_int) as libc::sighandler_t,
);
if before == libc::SIG_IGN {
libc::signal(signal, libc::SIG_IGN);
}
}
}
Some(ends[0])
}
extern "C" fn stay(_signal: libc::c_int) {}
fn wait_for_event(socket: RawFd, wake: RawFd) -> bool {
let mut waits = [
libc::pollfd {
fd: socket,
events: libc::POLLIN,
revents: 0,
},
libc::pollfd {
fd: wake,
events: libc::POLLIN,
revents: 0,
},
];
let ready = unsafe { libc::poll(waits.as_mut_ptr(), 2, -1) };
if ready <= 0 {
return false;
}
if waits[1].revents != 0 {
let mut drained = [0_u8; 64];
while unsafe { libc::read(wake, drained.as_mut_ptr().cast(), drained.len()) } > 0 {}
}
waits[0].revents != 0
}
fn handle_requests(socket: RawFd, inbox: &mut Inbox, held: &mut Vec<Held>) -> Option<bool> {
loop {
let frame = match inbox.next_frame() {
Ok(Some(frame)) => frame,
Ok(None) => return None,
Err(_) => return Some(false),
};
match Request::decode(&frame) {
Some(Request::Shutdown) => return Some(true),
Some(Request::Spawn { call, spec }) => {
let (Some(stdout), Some(stderr)) =
(inbox.take_descriptor(), inbox.take_descriptor())
else {
return Some(false);
};
#[cfg(any(test, feature = "test-support"))]
let started = start_with_fault(spec, stdout, stderr);
#[cfg(not(any(test, feature = "test-support")))]
let started = start(spec, stdout, stderr);
let reply = match started {
Ok(child) => {
let pid = child.id();
held.push(Held {
call,
child,
reported: false,
killed: false,
released: false,
});
Reply::Spawned { call, pid }
}
Err(error) => Reply::SpawnFailed {
call,
os_error: error.raw_os_error().unwrap_or(0),
message: error.to_string(),
},
};
if send_frame(socket, &reply.encode(), &[]).is_err() {
return Some(false);
}
}
Some(Request::Kill { call }) => {
if let Some(held) = held.iter_mut().find(|held| held.call == call) {
end_group_once(held);
}
}
Some(Request::Release { call }) => {
if let Some(held) = held.iter_mut().find(|held| held.call == call) {
held.released = true;
}
}
None => return Some(false),
}
}
}
fn start(spec: SpawnSpec, stdout: OwnedFd, stderr: OwnedFd) -> std::io::Result<Child> {
let mut command = Command::new(&spec.program);
command
.args(&spec.args)
.env_clear()
.envs(spec.env)
.stdin(Stdio::null())
.stdout(stdout)
.stderr(stderr);
if let Some(cwd) = &spec.cwd {
command.current_dir(cwd);
}
lead_new_session(&mut command);
command.spawn()
}
#[cfg(any(test, feature = "test-support"))]
fn start_with_fault(spec: SpawnSpec, stdout: OwnedFd, stderr: OwnedFd) -> std::io::Result<Child> {
match std::env::var(super::tests::FAULT_ENV).as_deref() {
Ok("refuse") => Err(std::io::Error::other("refused for a test")),
Ok("die-after-start") => {
let _started = start(spec, stdout, stderr)?;
std::thread::sleep(std::time::Duration::from_millis(700));
unsafe { libc::_exit(9) }
}
_ => start(spec, stdout, stderr),
}
}
fn end_group_once(held: &mut Held) {
if held.killed || held.released {
return;
}
held.killed = true;
#[cfg(any(test, feature = "test-support"))]
GROUP_SIGNAL_LOG.with(|log| {
log.borrow_mut()
.push(exited_unreaped(&mut held.child).is_ok());
});
let Ok(group) = crate::ipc::process::group_target(held.child.id()) else {
return;
};
unsafe { libc::killpg(group, libc::SIGKILL) };
}
fn report_exits(socket: RawFd, held: &mut Vec<Held>) -> bool {
let mut reachable = true;
held.retain_mut(|held| {
if !matches!(exited_unreaped(&mut held.child), Ok(true)) {
return true;
}
if !held.reported {
held.reported = true;
let reply = Reply::Exited {
call: held.call,
status: unreaped_status(&held.child),
};
reachable &= send_frame(socket, &reply.encode(), &[]).is_ok();
}
if held.killed || held.released {
let _ = held.child.wait();
return false;
}
true
});
reachable
}
fn unreaped_status(child: &Child) -> i32 {
let mut info: libc::siginfo_t = unsafe { std::mem::zeroed() };
let result = unsafe {
libc::waitid(
libc::P_PID,
child.id() as libc::id_t,
&raw mut info,
libc::WEXITED | libc::WNOHANG | libc::WNOWAIT,
)
};
if result != 0 {
return 1 << 8;
}
let status = unsafe { info.si_status() };
match info.si_code {
libc::CLD_EXITED => (status & 0xff) << 8,
libc::CLD_DUMPED => (status & 0x7f) | 0x80,
_ => status & 0x7f,
}
}
fn end_held(held: &mut Vec<Held>) {
for mut held in held.drain(..) {
if held.released {
continue;
}
let group = held.child.id() as libc::pid_t;
let _ = super::end_pinned_group(group, END_BUDGET);
let _ = held.child.wait();
}
}
pub(super) fn exit_status(raw: i32) -> std::process::ExitStatus {
std::process::ExitStatus::from_raw(raw)
}
#[cfg(any(test, feature = "test-support"))]
thread_local! {
static GROUP_SIGNAL_LOG: std::cell::RefCell<Vec<bool>> =
const { std::cell::RefCell::new(Vec::new()) };
}
#[cfg(any(test, feature = "test-support"))]
pub(super) fn kill_path_for_test(spec: SpawnSpec) -> (Vec<bool>, libc::pid_t, bool) {
let (_reader, writer) = std::io::pipe().expect("pipe");
let stderr = writer.try_clone().expect("clone");
let child = start(spec, writer.into(), stderr.into()).expect("the shell starts");
let group = child.id() as libc::pid_t;
let mut held = vec![Held {
call: 1,
child,
reported: true,
killed: false,
released: false,
}];
std::thread::sleep(std::time::Duration::from_millis(200));
end_group_once(&mut held[0]);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
while !held.is_empty() && std::time::Instant::now() < deadline {
report_exits(-1, &mut held);
std::thread::sleep(std::time::Duration::from_millis(5));
}
let reaped = held.is_empty();
let log = GROUP_SIGNAL_LOG.with(|log| std::mem::take(&mut *log.borrow_mut()));
(log, group, reaped)
}