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