mxsh 0.2.0

Embeddable POSIX-style shell parser and runtime
Documentation
use std::collections::BTreeMap;

use super::*;

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum ProcessGroupPlan {
    Inherit,
    New,
    Join(sys::ProcessHandle),
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum ChildFdDisposition {
    OpenFrom(sys::FileDescriptor),
    Closed,
}

#[derive(Clone, Debug)]
pub(super) struct ChildLaunchPlan {
    pub(super) program: String,
    pub(super) argv: Vec<String>,
    pub(super) env: Vec<(String, String)>,
    pub(super) cwd: PathBuf,
    pub(super) process_group: ProcessGroupPlan,
    pub(super) stdio: [ChildFdDisposition; 3],
    pub(super) extra_fds: BTreeMap<i32, ChildFdDisposition>,
    pub(super) close_fds: Vec<sys::FileDescriptor>,
    pub(super) signal_dispositions: sys::ChildSignalPlan,
}

impl ChildLaunchPlan {
    pub(super) fn new(
        state: &ShellState,
        program: impl Into<String>,
        argv: Vec<String>,
        process_group: ProcessGroupPlan,
    ) -> Self {
        let stdio = [
            disposition_from_fd(state.stdin_fd),
            disposition_from_fd(state.stdout_fd),
            disposition_from_fd(state.stderr_fd),
        ];
        let extra_fds = state
            .fd_table
            .raw_fds
            .iter()
            .filter_map(|(&child_fd, &parent_fd)| {
                if parent_fd == sys::FileDescriptor::INVALID {
                    Some((child_fd, ChildFdDisposition::Closed))
                } else if parent_fd.is_open() {
                    Some((child_fd, ChildFdDisposition::OpenFrom(parent_fd)))
                } else {
                    None
                }
            })
            .collect();
        let close_fds = state.untracked_ambient_fds();
        Self {
            program: program.into(),
            argv,
            env: shell_resolve::exported_exec_environment(state),
            cwd: state.path_state.cwd.clone(),
            process_group,
            stdio,
            extra_fds,
            close_fds,
            signal_dispositions: child_signal_plan(state),
        }
    }

    pub(super) fn with_env(mut self, env: Vec<(String, String)>) -> Self {
        self.env = env;
        self
    }

    pub(super) fn with_cwd(mut self, cwd: PathBuf) -> Self {
        self.cwd = cwd;
        self
    }

    pub(super) fn with_stdio(
        mut self,
        stdin: sys::FileDescriptor,
        stdout: sys::FileDescriptor,
        stderr: sys::FileDescriptor,
    ) -> Self {
        self.stdio = [
            disposition_from_fd(stdin),
            disposition_from_fd(stdout),
            disposition_from_fd(stderr),
        ];
        self
    }

    pub(super) fn with_extra_fd(mut self, child_fd: i32, disposition: ChildFdDisposition) -> Self {
        self.extra_fds.insert(child_fd, disposition);
        self
    }

    #[cfg(all(test, feature = "test-support", feature = "unix-runtime"))]
    pub(super) fn with_close_fds(mut self, close_fds: Vec<sys::FileDescriptor>) -> Self {
        self.close_fds = close_fds;
        self
    }

    pub(super) fn add_close_fds(mut self, close_fds: Vec<sys::FileDescriptor>) -> Self {
        self.close_fds.extend(close_fds);
        self
    }

    pub(super) fn spawn<R: Runtime>(
        &self,
        runtime: &mut R,
        mode: sys::SpawnMode,
    ) -> Result<sys::SpawnedProcess, io::Error> {
        match runtime.spawn_external_command(
            &self.external_command(),
            self.spawn_stdio(),
            &self.child_close_fds(),
            mode,
        ) {
            Err(err) if is_exec_format_error(&err) => runtime.spawn_external_command(
                &self.shell_script_fallback_command(),
                self.spawn_stdio(),
                &self.child_close_fds(),
                mode,
            ),
            result => result,
        }
    }

    pub(super) fn wait_foreground<R: Runtime>(
        &self,
        state: &mut ShellState,
        runtime: &mut R,
        child: sys::SpawnedProcess,
    ) -> i32 {
        let mut foreground_guard = if matches!(self.process_group, ProcessGroupPlan::New)
            && state.interactive
            && state.stdin_fd.is_valid()
        {
            runtime.claim_foreground(child.handle, state.stdin_fd).ok()
        } else {
            None
        };
        loop {
            let event = match runtime.wait_process(child.handle, sys::WaitMode::Block) {
                Ok(event) => event,
                Err(_) => {
                    if let Some(guard) = foreground_guard {
                        let _ = runtime.release_foreground(guard);
                    }
                    return 128;
                }
            };
            match shell_jobs::process_event_to_job_state(event) {
                JobState::Running => continue,
                JobState::Stopped(sig) => {
                    let background_job = matches!(
                        state.execution_context.kind,
                        ExecutionContextKind::BackgroundJob
                    );
                    if background_job && super::machine::take_background_continue_signal() {
                        continue_background_machine_child();
                        continue;
                    }
                    if stop_background_machine_for_child_stop(state, sig) {
                        continue;
                    }
                    let job_id = state.job_table.next_job_id;
                    state.job_table.next_job_id += 1;
                    state.job_table.jobs.push(ShellJob {
                        job_id,
                        handle: child.handle,
                        display_pid: child.display_pid,
                        state: JobState::Stopped(sig),
                        command_id: state
                            .active_command_id()
                            .map(ToString::to_string)
                            .unwrap_or_else(shell_events::new_command_id),
                        finish_emitted: false,
                    });
                    if let Some(guard) = foreground_guard.take() {
                        let _ = runtime.release_foreground(guard);
                    }
                    return normalize_exit_status(128_i64 + i64::from(sig));
                }
                JobState::Done(status) => {
                    if let Some(guard) = foreground_guard.take() {
                        let _ = runtime.release_foreground(guard);
                    }
                    return normalize_exit_status(status);
                }
            }
        }
    }

    pub(super) fn exec_replace<R: Runtime>(&self, runtime: &R) -> Result<(), io::Error> {
        match runtime.exec_replace_command(
            &self.external_command(),
            self.spawn_stdio(),
            &self.child_close_fds(),
        ) {
            Err(err) if is_exec_format_error(&err) => runtime.exec_replace_command(
                &self.shell_script_fallback_command(),
                self.spawn_stdio(),
                &self.child_close_fds(),
            ),
            result => result,
        }
    }

    pub(super) fn external_command(&self) -> sys::ExternalCommand {
        sys::ExternalCommand {
            program: self.program.clone(),
            argv: self.argv.clone(),
            env: self.env.clone(),
            cwd: self.cwd.clone(),
            create_process_group: matches!(self.process_group, ProcessGroupPlan::New),
            join_process_group: match self.process_group {
                ProcessGroupPlan::Join(handle) => Some(handle),
                ProcessGroupPlan::Inherit | ProcessGroupPlan::New => None,
            },
            passed_fds: self.passed_fds(),
            signal_plan: self.signal_dispositions.clone(),
        }
    }

    fn shell_script_fallback_command(&self) -> sys::ExternalCommand {
        let mut argv = Vec::with_capacity(self.argv.len() + 1);
        argv.push("sh".to_string());
        argv.push(self.program.clone());
        argv.extend(self.argv.iter().skip(1).cloned());

        sys::ExternalCommand {
            program: "/bin/sh".to_string(),
            argv,
            env: self.env.clone(),
            cwd: self.cwd.clone(),
            create_process_group: matches!(self.process_group, ProcessGroupPlan::New),
            join_process_group: match self.process_group {
                ProcessGroupPlan::Join(handle) => Some(handle),
                ProcessGroupPlan::Inherit | ProcessGroupPlan::New => None,
            },
            passed_fds: self.passed_fds(),
            signal_plan: self.signal_dispositions.clone(),
        }
    }

    pub(super) fn spawn_stdio(&self) -> sys::SpawnStdio {
        sys::SpawnStdio {
            stdin_fd: fd_for_disposition(self.stdio[0]),
            stdout_fd: fd_for_disposition(self.stdio[1]),
            stderr_fd: fd_for_disposition(self.stdio[2]),
        }
    }

    fn passed_fds(&self) -> Vec<sys::PassedFileDescriptor> {
        self.extra_fds
            .iter()
            .filter_map(|(&child_fd, disposition)| match disposition {
                ChildFdDisposition::OpenFrom(parent_fd) => Some(sys::PassedFileDescriptor {
                    parent_fd: *parent_fd,
                    child_fd: sys::FileDescriptor::new(child_fd),
                }),
                ChildFdDisposition::Closed => None,
            })
            .collect()
    }

    pub(super) fn child_close_fds(&self) -> Vec<sys::FileDescriptor> {
        let mut close_fds = self.close_fds.clone();
        for (&child_fd, disposition) in &self.extra_fds {
            if matches!(disposition, ChildFdDisposition::Closed) {
                close_fds.push(sys::FileDescriptor::new(child_fd));
            }
        }
        close_fds.sort_by_key(|fd| fd.as_i32());
        close_fds.dedup();
        close_fds.retain(|fd| {
            !self.child_fd_is_explicitly_open(fd.as_i32()) && !self.child_fd_is_open_source(*fd)
        });
        close_fds
    }

    fn child_fd_is_explicitly_open(&self, child_fd: i32) -> bool {
        if (0..=2).contains(&child_fd)
            && matches!(
                self.stdio[child_fd as usize],
                ChildFdDisposition::OpenFrom(_)
            )
        {
            return true;
        }
        matches!(
            self.extra_fds.get(&child_fd),
            Some(ChildFdDisposition::OpenFrom(_))
        )
    }

    fn child_fd_is_open_source(&self, fd: sys::FileDescriptor) -> bool {
        self.stdio
            .iter()
            .any(|disposition| matches!(disposition, ChildFdDisposition::OpenFrom(source) if *source == fd))
            || self
                .extra_fds
                .values()
                .any(|disposition| matches!(disposition, ChildFdDisposition::OpenFrom(source) if *source == fd))
    }
}

fn stop_background_machine_for_child_stop(state: &ShellState, sig: i32) -> bool {
    if !matches!(
        state.execution_context.kind,
        ExecutionContextKind::BackgroundJob
    ) {
        return false;
    }
    // Propagate a child-only stop to the parent shell by stopping the machine
    // wrapper; after `fg` resumes the wrapper, wake the child process too.
    stop_current_process_with_signal(sig);
    continue_background_machine_child();
    true
}

fn stop_current_process_with_signal(sig: i32) {
    unsafe {
        if sig == libc::SIGSTOP {
            libc::raise(libc::SIGSTOP);
            continue_current_process_group();
            return;
        }
        let previous = libc::signal(sig, libc::SIG_DFL);
        libc::raise(sig);
        if previous != libc::SIG_ERR {
            libc::signal(sig, previous);
        }
        continue_current_process_group();
    }
}

fn continue_current_process_group() {
    unsafe {
        libc::kill(0, libc::SIGCONT);
    }
}

fn continue_background_machine_child() {
    continue_current_process_group();
}

fn disposition_from_fd(fd: sys::FileDescriptor) -> ChildFdDisposition {
    if fd.is_open() {
        ChildFdDisposition::OpenFrom(fd)
    } else {
        ChildFdDisposition::Closed
    }
}

fn fd_for_disposition(disposition: ChildFdDisposition) -> sys::FileDescriptor {
    match disposition {
        ChildFdDisposition::OpenFrom(fd) => fd,
        ChildFdDisposition::Closed => sys::FileDescriptor::INVALID,
    }
}

fn is_exec_format_error(err: &io::Error) -> bool {
    err.raw_os_error() == Some(libc::ENOEXEC)
}

fn child_signal_plan(state: &ShellState) -> sys::ChildSignalPlan {
    let ignored_signals: Vec<i32> = state
        .trap_table
        .traps
        .iter()
        .filter_map(|(&sig, action)| (sig > 0 && action.is_empty()).then_some(sig))
        .collect();
    let default_signals = [libc::SIGTTOU, libc::SIGTTIN, libc::SIGTSTP, libc::SIGPIPE]
        .into_iter()
        .filter(|sig| !ignored_signals.contains(sig))
        .collect();
    sys::ChildSignalPlan {
        default_signals,
        ignored_signals,
    }
}