use std::cell::Cell;
use std::io;
use std::os::unix::process::CommandExt;
use std::process::{Child, Command, Stdio};
use std::rc::Rc;
use calloop::Interest;
use rustix::process::{Pid, PidfdFlags};
use crate::Shared;
pub(crate) fn spawn(shared: &Rc<Shared>, command: &mut Command) -> io::Result<u32> {
let program = command.get_program().to_string_lossy().into_owned();
untune(command);
let child = command
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.process_group(0)
.spawn()
.inspect_err(|error| tracing::warn!(%error, program, "couldn't start a program"))?;
let pid = child.id();
reap(shared, child, program);
Ok(pid)
}
const TUNABLE: &str = "glibc.malloc.mmap_threshold=131072";
pub(crate) fn untune(command: &mut Command) {
untune_from(command, std::env::var_os("GLIBC_TUNABLES"));
}
fn untune_from(command: &mut Command, ours: Option<std::ffi::OsString>) {
command.env_remove("CRABSHELL_CONTROL");
if ours.is_some_and(|value| value == TUNABLE) {
command.env_remove("GLIBC_TUNABLES");
}
}
pub(crate) fn reap(shared: &Rc<Shared>, mut child: Child, program: String) {
let pidfd = match rustix::process::pidfd_open(Pid::from_child(&child), PidfdFlags::empty()) {
Ok(pidfd) => pidfd,
Err(error) => {
tracing::warn!(%error, program, "can't watch a program, so it stays a zombie once it exits");
return;
}
};
let token = Rc::new(Cell::new(None));
let reap = {
let (shared, token) = (Rc::clone(shared), Rc::clone(&token));
Box::new(move || {
match child.wait() {
Ok(status) => tracing::debug!(program, %status, "a program ended"),
Err(error) => tracing::warn!(%error, program, "couldn't reap a program"),
}
if let (Some(token), Some(event_loop)) = (token.take(), shared.event_loop()) {
event_loop.remove(token);
}
})
};
if let Some(event_loop) = shared.event_loop() {
token.set(event_loop.insert_fd(pidfd, Interest::READ, reap));
}
}
#[cfg(test)]
mod tests {
use std::process::Command;
use std::rc::Rc;
use std::time::{Duration, Instant};
use super::spawn;
use crate::Shared;
use crate::testing::{Harness, capture_logs, offline};
fn shared(shell: &Harness) -> Rc<Shared> {
Rc::new(Shared::new(
shell.cx.owner(),
&shell.event_loop.handle(),
offline(),
))
}
#[test]
fn a_program_runs_apart_and_is_reaped_when_it_exits() {
let mut shell = Harness::new(offline());
let shared = shared(&shell);
let pid = spawn(&shared, Command::new("sleep").arg("0.3")).expect("sleep starts");
let proc = format!("/proc/{pid}");
let stat = std::fs::read_to_string(format!("{proc}/stat")).expect("it runs");
let fields: Vec<&str> = stat
.rsplit_once(')')
.expect("a stat line")
.1
.split_whitespace()
.collect();
assert_eq!(fields[2], pid.to_string(), "its own process group: {stat}");
for fd in 0..3 {
let target = std::fs::read_link(format!("{proc}/fd/{fd}")).expect("an fd");
assert_eq!(target, std::path::Path::new("/dev/null"), "fd {fd}");
}
let wakeups = shell.run_until(Instant::now() + Duration::from_millis(800));
assert!(wakeups <= 2, "{wakeups} wakeups");
assert!(
!std::path::Path::new(&proc).exists(),
"reaped, not a zombie"
);
let wakeups = shell.run_until(Instant::now() + Duration::from_millis(200));
assert_eq!(wakeups, 1);
}
#[test]
fn a_program_that_cant_start_is_logged_and_returned() {
let shell = Harness::new(offline());
let shared = shared(&shell);
let logs = capture_logs(|| {
let started = spawn(&shared, &mut Command::new("/nonexistent/crabshell-program"));
assert!(started.is_err());
});
assert!(logs.contains("couldn't start"), "{logs}");
assert!(logs.contains("/nonexistent/crabshell-program"), "{logs}");
}
#[test]
fn the_shells_own_glibc_tunable_isnt_passed_on() {
use std::ffi::OsStr;
let removed = |ours: Option<&str>| {
let mut command = Command::new("true");
super::untune_from(&mut command, ours.map(Into::into));
command
.get_envs()
.any(|(name, value)| name == OsStr::new("GLIBC_TUNABLES") && value.is_none())
};
assert!(removed(Some(super::TUNABLE)));
assert!(!removed(Some("glibc.malloc.arena_max=2")), "the user's own");
assert!(!removed(None));
let mut command = Command::new("true");
super::untune_from(&mut command, None);
let control = command
.get_envs()
.any(|(name, value)| name == OsStr::new("CRABSHELL_CONTROL") && value.is_none());
assert!(control, "the supervisor's socket isn't passed on");
}
}