use std::ffi::CString;
use std::io;
use std::os::unix::ffi::OsStrExt;
use std::path::Path;
use super::fd::{FileDescriptor, fd_has_cloexec};
use super::unix_exec;
use super::{
ChildSignalPlan, ExternalCommand, PassedFileDescriptor, ProcessHandle, SignalDispositionGuard,
SpawnStdio,
};
unsafe extern "C" {
fn posix_spawn_file_actions_addchdir_np(
actions: *mut libc::posix_spawn_file_actions_t,
path: *const libc::c_char,
) -> libc::c_int;
}
pub(crate) enum FdAction {
Close(FileDescriptor),
}
fn cstring_arg(s: &str) -> Result<CString, io::Error> {
CString::new(s)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "argument contains NUL"))
}
fn cstring_env(key: &str, value: &str) -> Result<CString, io::Error> {
CString::new(format!("{key}={value}")).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidInput,
"environment entry contains NUL",
)
})
}
pub fn exec(
program: &str,
argv: &[String],
env: &[(String, String)],
cwd: &Path,
) -> Result<(), io::Error> {
unix_exec::exec_replace(program, argv, env, cwd)
}
pub(crate) fn spawn_command(
command: &ExternalCommand,
create_process_group: bool,
join_process_group: Option<ProcessHandle>,
stdio: SpawnStdio,
fd_actions: &[FdAction],
) -> Result<libc::pid_t, io::Error> {
spawn(SpawnRequest {
program: &command.program,
argv: &command.argv,
env: &command.env,
cwd: &command.cwd,
create_process_group,
join_process_group,
passed_fds: &command.passed_fds,
signal_plan: &command.signal_plan,
stdio,
fd_actions,
})
}
pub(crate) fn pid_from_handle(handle: ProcessHandle) -> Result<libc::pid_t, io::Error> {
if handle.as_u64() > libc::pid_t::MAX as u64 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"process handle is out of range for pid_t",
));
}
Ok(handle.as_u64() as libc::pid_t)
}
pub(crate) struct SpawnRequest<'a> {
pub(crate) program: &'a str,
pub(crate) argv: &'a [String],
pub(crate) env: &'a [(String, String)],
pub(crate) cwd: &'a Path,
pub(crate) create_process_group: bool,
pub(crate) join_process_group: Option<ProcessHandle>,
pub(crate) passed_fds: &'a [PassedFileDescriptor],
pub(crate) signal_plan: &'a ChildSignalPlan,
pub(crate) stdio: SpawnStdio,
pub(crate) fd_actions: &'a [FdAction],
}
fn add_close_action(
file_actions: &mut libc::posix_spawn_file_actions_t,
fd: FileDescriptor,
) -> Result<(), io::Error> {
let ret = unsafe { libc::posix_spawn_file_actions_addclose(file_actions, fd.into_raw_fd()) };
if ret == 0 || ret == libc::EBADF {
Ok(())
} else {
Err(io::Error::from_raw_os_error(ret))
}
}
fn fd_is_open(fd: FileDescriptor) -> Result<bool, io::Error> {
match fd_has_cloexec(fd.into_raw_fd()) {
Ok(_) => Ok(true),
Err(err) if err.raw_os_error() == Some(libc::EBADF) => Ok(false),
Err(err) => Err(err),
}
}
fn ignored_signal_guard(signals: &[i32]) -> SignalDispositionGuard {
let mut guard = SignalDispositionGuard::new();
for &sig in signals {
guard.set(sig, libc::SIG_IGN);
}
guard
}
pub(crate) fn spawn(request: SpawnRequest<'_>) -> Result<libc::pid_t, io::Error> {
let c_prog = CString::new(request.program)
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "program contains NUL"))?;
let c_cwd = CString::new(request.cwd.as_os_str().as_bytes())
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "cwd contains NUL"))?;
let c_argv: Vec<CString> = request
.argv
.iter()
.map(|a| cstring_arg(a))
.collect::<Result<_, _>>()?;
let mut c_argv_ptrs: Vec<*mut libc::c_char> = c_argv
.iter()
.map(|a| a.as_ptr() as *mut libc::c_char)
.collect();
c_argv_ptrs.push(std::ptr::null_mut());
let c_env: Vec<CString> = request
.env
.iter()
.map(|(k, v)| cstring_env(k, v))
.collect::<Result<_, _>>()?;
let mut c_env_ptrs: Vec<*mut libc::c_char> = c_env
.iter()
.map(|e| e.as_ptr() as *mut libc::c_char)
.collect();
c_env_ptrs.push(std::ptr::null_mut());
let mut file_actions: libc::posix_spawn_file_actions_t = unsafe { std::mem::zeroed() };
let init_ret = unsafe { libc::posix_spawn_file_actions_init(&mut file_actions) };
if init_ret != 0 {
return Err(io::Error::from_raw_os_error(init_ret));
}
if let Err(err) = add_spawn_chdir_action(&mut file_actions, &c_cwd, request.cwd) {
unsafe { libc::posix_spawn_file_actions_destroy(&mut file_actions) };
return Err(err);
}
let mut spawn_stdio = request.stdio;
let mut parent_temp_stdio = Vec::new();
for (from_fd, to_fd) in [
(&mut spawn_stdio.stdin_fd, FileDescriptor::STDIN),
(&mut spawn_stdio.stdout_fd, FileDescriptor::STDOUT),
(&mut spawn_stdio.stderr_fd, FileDescriptor::STDERR),
] {
if *from_fd != to_fd {
continue;
}
if !fd_has_cloexec(from_fd.into_raw_fd())? {
continue;
}
let duped = from_fd.dup()?;
*from_fd = duped;
parent_temp_stdio.push(duped);
}
let requested_closes: Vec<_> = request
.fd_actions
.iter()
.map(|action| match action {
FdAction::Close(fd) => fd.as_i32(),
})
.collect();
let mut explicit_open_fds = Vec::new();
for (from_fd, to_fd) in [
(spawn_stdio.stdin_fd, FileDescriptor::STDIN),
(spawn_stdio.stdout_fd, FileDescriptor::STDOUT),
(spawn_stdio.stderr_fd, FileDescriptor::STDERR),
] {
if from_fd.is_valid() {
explicit_open_fds.push(to_fd.as_i32());
}
}
for passed_fd in request.passed_fds {
if passed_fd.parent_fd.is_valid() {
explicit_open_fds.push(passed_fd.child_fd.as_i32());
}
}
explicit_open_fds.sort_unstable();
explicit_open_fds.dedup();
let mut child_closes_after_dup = Vec::new();
for (from_fd, to_fd) in [
(spawn_stdio.stdin_fd, FileDescriptor::STDIN),
(spawn_stdio.stdout_fd, FileDescriptor::STDOUT),
(spawn_stdio.stderr_fd, FileDescriptor::STDERR),
] {
if !from_fd.is_valid() {
if let Err(err) = add_close_action(&mut file_actions, to_fd) {
for fd in parent_temp_stdio {
fd.close();
}
unsafe { libc::posix_spawn_file_actions_destroy(&mut file_actions) };
return Err(err);
}
continue;
}
if from_fd == to_fd {
continue;
}
let ret = unsafe {
libc::posix_spawn_file_actions_adddup2(
&mut file_actions,
from_fd.into_raw_fd(),
to_fd.into_raw_fd(),
)
};
if ret != 0 {
for fd in parent_temp_stdio {
fd.close();
}
unsafe { libc::posix_spawn_file_actions_destroy(&mut file_actions) };
return Err(io::Error::from_raw_os_error(ret));
}
if from_fd.as_i32() > 2
&& !requested_closes.contains(&from_fd.as_i32())
&& !explicit_open_fds.contains(&from_fd.as_i32())
{
child_closes_after_dup.push(from_fd);
}
}
for passed_fd in request.passed_fds {
let mut parent_fd = passed_fd.parent_fd;
let mut close_parent_fd_immediately = true;
let parent_has_cloexec = match fd_has_cloexec(parent_fd.into_raw_fd()) {
Ok(has_cloexec) => has_cloexec,
Err(err) if err.raw_os_error() == Some(libc::EBADF) => continue,
Err(err) => return Err(err),
};
if parent_has_cloexec {
let duped = parent_fd.dup()?;
parent_fd = duped;
parent_temp_stdio.push(duped);
close_parent_fd_immediately = false;
}
let ret = unsafe {
libc::posix_spawn_file_actions_adddup2(
&mut file_actions,
parent_fd.into_raw_fd(),
passed_fd.child_fd.into_raw_fd(),
)
};
if ret != 0 {
for fd in parent_temp_stdio {
fd.close();
}
unsafe { libc::posix_spawn_file_actions_destroy(&mut file_actions) };
return Err(io::Error::from_raw_os_error(ret));
}
if parent_fd != passed_fd.child_fd
&& close_parent_fd_immediately
&& parent_fd.as_i32() > 2
&& !requested_closes.contains(&parent_fd.as_i32())
&& !explicit_open_fds.contains(&parent_fd.as_i32())
{
child_closes_after_dup.push(parent_fd);
}
}
child_closes_after_dup.sort_by_key(|fd| fd.as_i32());
child_closes_after_dup.dedup();
for fd in &child_closes_after_dup {
if let Err(err) = add_close_action(&mut file_actions, *fd) {
for fd in parent_temp_stdio {
fd.close();
}
unsafe { libc::posix_spawn_file_actions_destroy(&mut file_actions) };
return Err(err);
}
}
for action in request.fd_actions {
match action {
FdAction::Close(fd) => {
if !fd_is_open(*fd)? {
continue;
}
if let Err(err) = add_close_action(&mut file_actions, *fd) {
for fd in parent_temp_stdio {
fd.close();
}
unsafe { libc::posix_spawn_file_actions_destroy(&mut file_actions) };
return Err(err);
}
}
}
}
for fd in &parent_temp_stdio {
if let Err(err) = add_close_action(&mut file_actions, *fd) {
for fd in parent_temp_stdio {
fd.close();
}
unsafe { libc::posix_spawn_file_actions_destroy(&mut file_actions) };
return Err(err);
}
}
let mut attr: libc::posix_spawnattr_t = unsafe { std::mem::zeroed() };
let attr_ret = unsafe { libc::posix_spawnattr_init(&mut attr) };
if attr_ret != 0 {
for fd in parent_temp_stdio {
fd.close();
}
unsafe { libc::posix_spawn_file_actions_destroy(&mut file_actions) };
return Err(io::Error::from_raw_os_error(attr_ret));
}
if let Err(err) = configure_spawn_attributes(
&mut attr,
request.create_process_group,
request.join_process_group,
request.signal_plan,
) {
for fd in parent_temp_stdio {
fd.close();
}
unsafe {
libc::posix_spawnattr_destroy(&mut attr);
libc::posix_spawn_file_actions_destroy(&mut file_actions);
}
return Err(err);
}
let mut pid: libc::pid_t = 0;
let _signal_guard = ignored_signal_guard(&request.signal_plan.ignored_signals);
let ret = unsafe {
libc::posix_spawnp(
&mut pid,
c_prog.as_ptr(),
&file_actions,
&attr,
c_argv_ptrs.as_ptr(),
c_env_ptrs.as_ptr(),
)
};
unsafe {
libc::posix_spawnattr_destroy(&mut attr);
libc::posix_spawn_file_actions_destroy(&mut file_actions);
}
for fd in parent_temp_stdio {
fd.close();
}
if ret != 0 {
Err(io::Error::from_raw_os_error(ret))
} else {
Ok(pid)
}
}
fn configure_spawn_attributes(
attr: &mut libc::posix_spawnattr_t,
create_process_group: bool,
join_process_group: Option<ProcessHandle>,
signal_plan: &ChildSignalPlan,
) -> Result<(), io::Error> {
let mut defaults: libc::sigset_t = unsafe { std::mem::zeroed() };
if unsafe { libc::sigemptyset(&mut defaults) } != 0 {
return Err(io::Error::last_os_error());
}
for &sig in &signal_plan.default_signals {
if unsafe { libc::sigaddset(&mut defaults, sig) } != 0 {
return Err(io::Error::last_os_error());
}
}
let ret = unsafe { libc::posix_spawnattr_setsigdefault(attr, &defaults) };
if ret != 0 {
return Err(io::Error::from_raw_os_error(ret));
}
let mut flags = libc::POSIX_SPAWN_SETSIGDEF as libc::c_short;
if let Some(process_group) = join_process_group {
let process_group = pid_from_handle(process_group)?;
let ret = unsafe { libc::posix_spawnattr_setpgroup(attr, process_group) };
if ret != 0 {
return Err(io::Error::from_raw_os_error(ret));
}
flags |= libc::POSIX_SPAWN_SETPGROUP as libc::c_short;
} else if create_process_group {
let ret = unsafe { libc::posix_spawnattr_setpgroup(attr, 0) };
if ret != 0 {
return Err(io::Error::from_raw_os_error(ret));
}
flags |= libc::POSIX_SPAWN_SETPGROUP as libc::c_short;
}
let ret = unsafe { libc::posix_spawnattr_setflags(attr, flags) };
if ret != 0 {
return Err(io::Error::from_raw_os_error(ret));
}
Ok(())
}
#[cfg(any(
target_os = "macos",
target_os = "ios",
target_os = "linux",
target_os = "android"
))]
fn add_spawn_chdir_action(
file_actions: &mut libc::posix_spawn_file_actions_t,
c_cwd: &CString,
_cwd: &Path,
) -> Result<(), io::Error> {
let ret = unsafe { posix_spawn_file_actions_addchdir_np(file_actions, c_cwd.as_ptr()) };
if ret != 0 {
return Err(io::Error::from_raw_os_error(ret));
}
Ok(())
}
#[cfg(not(any(
target_os = "macos",
target_os = "ios",
target_os = "linux",
target_os = "android"
)))]
fn add_spawn_chdir_action(
_file_actions: &mut libc::posix_spawn_file_actions_t,
_c_cwd: &CString,
cwd: &Path,
) -> Result<(), io::Error> {
let here = std::env::current_dir()?;
if here == cwd {
Ok(())
} else {
Err(io::Error::new(
io::ErrorKind::Unsupported,
"spawn cwd changes are unsupported on this platform",
))
}
}