blob: d6047d96ac395ad080f2d58b2e73eac77d04dd8a [file] [log] [blame]
//! Implements run_command on posix using posix_spawn.
//! See run_command comments for why.
use crate::process::Termination;
use std::io::{Error, Read};
use std::os::fd::FromRawFd;
use std::os::unix::process::ExitStatusExt;
// https://github.com/rust-lang/libc/issues/2520
// libc crate doesn't expose the 'environ' pointer.
extern "C" {
static environ: *const *mut libc::c_char;
}
fn check_posix_spawn(func: &str, ret: libc::c_int) -> anyhow::Result<()> {
if ret != 0 {
let err_str = unsafe { std::ffi::CStr::from_ptr(libc::strerror(ret)) };
anyhow::bail!("{}: {}", func, err_str.to_str().unwrap());
}
Ok(())
}
fn check_ret_errno(func: &str, ret: libc::c_int) -> anyhow::Result<()> {
if ret < 0 {
let errno = Error::last_os_error().raw_os_error().unwrap();
let err_str = unsafe { std::ffi::CStr::from_ptr(libc::strerror(errno)) };
anyhow::bail!("{}: {}", func, err_str.to_str().unwrap());
}
Ok(())
}
/// Wraps libc::posix_spawnattr_t, in particular to implement Drop.
struct PosixSpawnAttr(libc::posix_spawnattr_t);
impl PosixSpawnAttr {
fn new() -> anyhow::Result<Self> {
unsafe {
let mut attr: libc::posix_spawnattr_t = std::mem::zeroed();
check_posix_spawn(
"posix_spawnattr_init",
libc::posix_spawnattr_init(&mut attr),
)?;
Ok(Self(attr))
}
}
fn as_ptr(&mut self) -> *mut libc::posix_spawnattr_t {
&mut self.0
}
fn setflags(&mut self, flags: libc::c_short) -> anyhow::Result<()> {
unsafe {
check_posix_spawn(
"posix_spawnattr_setflags",
libc::posix_spawnattr_setflags(self.as_ptr(), flags),
)
}
}
}
impl Drop for PosixSpawnAttr {
fn drop(&mut self) {
unsafe {
libc::posix_spawnattr_destroy(self.as_ptr());
}
}
}
/// Wraps libc::posix_spawn_file_actions_t, in particular to implement Drop.
struct PosixSpawnFileActions(libc::posix_spawn_file_actions_t);
impl PosixSpawnFileActions {
fn new() -> anyhow::Result<Self> {
unsafe {
let mut actions: libc::posix_spawn_file_actions_t = std::mem::zeroed();
check_posix_spawn(
"posix_spawn_file_actions_init",
libc::posix_spawn_file_actions_init(&mut actions),
)?;
Ok(Self(actions))
}
}
fn as_ptr(&mut self) -> *mut libc::posix_spawn_file_actions_t {
&mut self.0
}
fn addopen(
&mut self,
fd: i32,
path: &std::ffi::CStr,
oflag: i32,
mode: libc::mode_t,
) -> anyhow::Result<()> {
unsafe {
check_posix_spawn(
"posix_spawn_file_actions_addopen",
libc::posix_spawn_file_actions_addopen(
self.as_ptr(),
fd,
path.as_ptr(),
oflag,
mode,
),
)
}
}
fn adddup2(&mut self, fd: i32, newfd: i32) -> anyhow::Result<()> {
unsafe {
check_posix_spawn(
"posix_spawn_file_actions_adddup2",
libc::posix_spawn_file_actions_adddup2(self.as_ptr(), fd, newfd),
)
}
}
fn addclose(&mut self, fd: i32) -> anyhow::Result<()> {
unsafe {
check_posix_spawn(
"posix_spawn_file_actions_addclose",
libc::posix_spawn_file_actions_addclose(self.as_ptr(), fd),
)
}
}
}
impl Drop for PosixSpawnFileActions {
fn drop(&mut self) {
unsafe { libc::posix_spawn_file_actions_destroy(&mut self.0) };
}
}
/// Create an anonymous pipe as in libc::pipe(), but using pipe2() when available
/// to set CLOEXEC flag.
fn pipe2() -> anyhow::Result<[libc::c_int; 2]> {
// Compare to: https://doc.rust-lang.org/src/std/sys/unix/pipe.rs.html
unsafe {
let mut pipe: [libc::c_int; 2] = std::mem::zeroed();
// Mac: specially handled below with POSIX_SPAWN_CLOEXEC_DEFAULT
#[cfg(target_os = "macos")]
check_ret_errno("pipe", libc::pipe(pipe.as_mut_ptr()))?;
// Assume all non-Mac have pipe2; we can refine this on user feedback.
#[cfg(all(unix, not(target_os = "macos")))]
check_ret_errno("pipe", libc::pipe2(pipe.as_mut_ptr(), libc::O_CLOEXEC))?;
Ok(pipe)
}
}
pub fn run_command(cmdline: &str, mut output_cb: impl FnMut(&[u8])) -> anyhow::Result<Termination> {
// Spawn the subprocess using posix_spawn with output redirected to the pipe.
// We don't use Rust's process spawning because of issue #14 and because
// we want to feed both stdout and stderr into the same pipe, which cannot
// be done with the existing std::process API.
let (pid, mut pipe) = unsafe {
let pipe = pipe2()?;
let mut attr = PosixSpawnAttr::new()?;
// Apple-specific extension: close any open fds.
#[cfg(target_os = "macos")]
attr.setflags(libc::POSIX_SPAWN_CLOEXEC_DEFAULT as _)?;
let mut actions = PosixSpawnFileActions::new()?;
// open /dev/null over stdin
actions.addopen(
0,
std::ffi::CStr::from_bytes_with_nul_unchecked(b"/dev/null\0"),
libc::O_RDONLY,
0,
)?;
// stdout/stderr => pipe
actions.adddup2(pipe[1], 1)?;
actions.adddup2(pipe[1], 2)?;
// close pipe in child
actions.addclose(pipe[0])?;
actions.addclose(pipe[1])?;
let mut pid: libc::pid_t = 0;
let path = std::ffi::CStr::from_bytes_with_nul_unchecked(b"/bin/sh\0");
let cmdline_nul = std::ffi::CString::new(cmdline).unwrap();
let argv: [*const libc::c_char; 4] = [
path.as_ptr(),
b"-c\0".as_ptr() as *const _,
cmdline_nul.as_ptr(),
std::ptr::null(),
];
check_posix_spawn(
"posix_spawn",
libc::posix_spawn(
&mut pid,
path.as_ptr(),
actions.as_ptr(),
attr.as_ptr(),
// posix_spawn wants mutable argv:
// https://stackoverflow.com/questions/50596439/can-string-literals-be-passed-in-posix-spawns-argv
argv.as_ptr() as *const *mut _,
environ,
),
)?;
check_ret_errno("close", libc::close(pipe[1]))?;
(pid, std::fs::File::from_raw_fd(pipe[0]))
};
let mut buf: [u8; 4 << 10] = [0; 4 << 10];
loop {
let n = pipe.read(&mut buf)?;
if n == 0 {
break;
}
output_cb(&buf[0..n]);
}
drop(pipe);
let status = unsafe {
let mut status: i32 = 0;
check_ret_errno("waitpid", libc::waitpid(pid, &mut status, 0))?;
std::process::ExitStatus::from_raw(status)
};
let termination = if status.success() {
Termination::Success
} else if let Some(sig) = status.signal() {
match sig {
libc::SIGINT => {
output_cb("interrupted".as_bytes());
Termination::Interrupted
}
_ => {
output_cb(format!("signal {}", sig).as_bytes());
Termination::Failure
}
}
} else {
Termination::Failure
};
Ok(termination)
}