mxsh 0.1.0

Embeddable POSIX-style shell parser and runtime
Documentation
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::{ExternalCommand, PassedFileDescriptor, ProcessHandle, 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;
}

/// A file descriptor action for `posix_spawnp`.
pub(crate) enum FdAction {
    /// Close `fd` in the child.
    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",
        )
    })
}

/// Replace the current process with a new program via `execve` and PATH search.
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,
    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,
        passed_fds: &command.passed_fds,
        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) passed_fds: &'a [PassedFileDescriptor],
    pub(crate) stdio: SpawnStdio,
    pub(crate) fd_actions: &'a [FdAction],
}

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);
    }

    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 == 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));
        }
    }

    for passed_fd in request.passed_fds {
        let mut parent_fd = passed_fd.parent_fd;
        let mut close_parent_fd_immediately = true;
        if fd_has_cloexec(parent_fd.into_raw_fd())? {
            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 {
            continue;
        }
        let ret = unsafe {
            libc::posix_spawn_file_actions_addclose(&mut file_actions, parent_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));
        }
    }

    for action in request.fd_actions {
        match action {
            FdAction::Close(fd) => {
                let ret = unsafe {
                    libc::posix_spawn_file_actions_addclose(&mut file_actions, 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));
                }
            }
        }
    }

    for fd in &parent_temp_stdio {
        let ret =
            unsafe { libc::posix_spawn_file_actions_addclose(&mut file_actions, 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));
        }
    }

    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) {
        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 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,
) -> 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 [libc::SIGTTOU, libc::SIGTTIN, libc::SIGTSTP, libc::SIGPIPE] {
        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 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",
        ))
    }
}