use crate::job::Pid;
use crate::job::ProcessResult;
use crate::job::ProcessState;
use crate::signal;
use crate::stack::Frame;
use crate::system::ChildProcessTask;
use crate::system::Errno;
use crate::system::SigmaskOp;
use crate::system::System;
use crate::system::SystemEx;
use crate::Env;
use std::future::Future;
use std::pin::Pin;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum JobControl {
Foreground,
Background,
}
#[must_use = "a subshell is not started unless you call `Subshell::start`"]
pub struct Subshell<F> {
task: F,
job_control: Option<JobControl>,
ignores_sigint_sigquit: bool,
}
impl<F> std::fmt::Debug for Subshell<F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Subshell").finish_non_exhaustive()
}
}
impl<F> Subshell<F>
where
F: for<'a> FnOnce(&'a mut Env, Option<JobControl>) -> Pin<Box<dyn Future<Output = ()> + 'a>>
+ 'static,
{
pub fn new(task: F) -> Self {
Subshell {
task,
job_control: None,
ignores_sigint_sigquit: false,
}
}
pub fn job_control<J: Into<Option<JobControl>>>(mut self, job_control: J) -> Self {
self.job_control = job_control.into();
self
}
pub fn ignore_sigint_sigquit(mut self, ignore: bool) -> Self {
self.ignores_sigint_sigquit = ignore;
self
}
pub async fn start(self, env: &mut Env) -> Result<(Pid, Option<JobControl>), Errno> {
let job_control = env.controls_jobs().then_some(self.job_control).flatten();
let tty = match job_control {
None | Some(JobControl::Background) => None,
Some(JobControl::Foreground) => Some(env.get_tty()?),
};
let mut mask_guard = MaskGuard::new(env);
let ignore_sigint_sigquit = self.ignores_sigint_sigquit
&& job_control.is_none()
&& mask_guard.block_sigint_sigquit();
let keep_internal_dispositions_for_stoppers = job_control.is_none();
const ME: Pid = Pid(0);
let task: ChildProcessTask = Box::new(move |env| {
Box::pin(async move {
let mut env = env.push_frame(Frame::Subshell);
let env = &mut *env;
if let Some(job_control) = job_control {
if let Ok(()) = env.system.setpgid(ME, ME) {
match job_control {
JobControl::Background => (),
JobControl::Foreground => {
if let Some(tty) = tty {
let pgid = env.system.getpgrp();
let _ = env.system.tcsetpgrp_with_block(tty, pgid);
}
}
}
}
}
env.jobs.disown_all();
env.traps.enter_subshell(
&mut env.system,
ignore_sigint_sigquit,
keep_internal_dispositions_for_stoppers,
);
(self.task)(env, job_control).await
})
});
let child = mask_guard.env.system.new_child_process()?;
let child_pid = child(mask_guard.env, task).await;
if job_control.is_some() {
let _ = mask_guard.env.system.setpgid(child_pid, ME);
}
Ok((child_pid, job_control))
}
pub async fn start_and_wait(self, env: &mut Env) -> Result<(Pid, ProcessResult), Errno> {
let (pid, job_control) = self.start(env).await?;
let result = loop {
let state = env.wait_for_subshell(pid).await?.1;
if let ProcessState::Halted(result) = state {
if !result.is_stopped() || job_control.is_some() {
break result;
}
}
};
if job_control == Some(JobControl::Foreground) {
if let Some(tty) = env.tty {
env.system.tcsetpgrp_with_block(tty, env.main_pgid).ok();
}
}
Ok((pid, result))
}
}
#[derive(Debug)]
struct MaskGuard<'a> {
env: &'a mut Env,
old_mask: Option<Vec<signal::Number>>,
}
impl<'a> MaskGuard<'a> {
fn new(env: &'a mut Env) -> Self {
let old_mask = None;
Self { env, old_mask }
}
fn block_sigint_sigquit(&mut self) -> bool {
assert_eq!(self.old_mask, None);
let Some(sigint) = self.env.system.signal_number_from_name(signal::Name::Int) else {
return false;
};
let Some(sigquit) = self.env.system.signal_number_from_name(signal::Name::Quit) else {
return false;
};
let mut old_mask = Vec::new();
let success = self
.env
.system
.sigmask(
Some((SigmaskOp::Add, &[sigint, sigquit])),
Some(&mut old_mask),
)
.is_ok();
if success {
self.old_mask = Some(old_mask);
}
success
}
}
impl<'a> Drop for MaskGuard<'a> {
fn drop(&mut self) {
if let Some(old_mask) = &self.old_mask {
self.env
.system
.sigmask(Some((SigmaskOp::Set, old_mask)), None)
.ok();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::job::Job;
use crate::option::Option::{Interactive, Monitor};
use crate::option::State::On;
use crate::semantics::ExitStatus;
use crate::system::r#virtual::Inode;
use crate::system::r#virtual::SystemState;
use crate::system::r#virtual::{SIGCHLD, SIGINT, SIGQUIT, SIGTSTP, SIGTTIN, SIGTTOU};
use crate::system::Disposition;
use crate::system::Errno;
use crate::tests::in_virtual_system;
use crate::trap::Action;
use assert_matches::assert_matches;
use futures_executor::LocalPool;
use std::cell::Cell;
use std::cell::RefCell;
use std::rc::Rc;
use yash_syntax::source::Location;
fn stub_tty(state: &RefCell<SystemState>) {
state
.borrow_mut()
.file_system
.save("/dev/tty", Rc::new(RefCell::new(Inode::new([]))))
.unwrap();
}
#[test]
fn subshell_start_returns_child_process_id() {
in_virtual_system(|mut env, _state| async move {
let parent_pid = env.main_pid;
let child_pid = Rc::new(Cell::new(None));
let child_pid_2 = Rc::clone(&child_pid);
let subshell = Subshell::new(move |env, _job_control| {
Box::pin(async move {
child_pid_2.set(Some(env.system.getpid()));
assert_eq!(env.system.getppid(), parent_pid);
})
});
let result = subshell.start(&mut env).await.unwrap().0;
env.wait_for_subshell(result).await.unwrap();
assert_eq!(Some(result), child_pid.get());
});
}
#[test]
fn subshell_start_failing() {
let mut executor = LocalPool::new();
let env = &mut Env::new_virtual();
let subshell =
Subshell::new(|_env, _job_control| unreachable!("subshell not expected to run"));
let result = executor.run_until(subshell.start(env));
assert_eq!(result, Err(Errno::ENOSYS));
}
#[test]
fn stack_frame_in_subshell() {
in_virtual_system(|mut env, _state| async move {
let subshell = Subshell::new(|env, _job_control| {
Box::pin(async { assert_eq!(env.stack[..], [Frame::Subshell]) })
});
let pid = subshell.start(&mut env).await.unwrap().0;
assert_eq!(env.stack[..], []);
env.wait_for_subshell(pid).await.unwrap();
});
}
#[test]
fn jobs_disowned_in_subshell() {
in_virtual_system(|mut env, _state| async move {
let index = env.jobs.add(Job::new(Pid(123)));
let subshell = Subshell::new(move |env, _job_control| {
Box::pin(async move { assert!(!env.jobs[index].is_owned) })
});
let pid = subshell.start(&mut env).await.unwrap().0;
env.wait_for_subshell(pid).await.unwrap();
assert!(env.jobs[index].is_owned);
});
}
#[test]
fn trap_reset_in_subshell() {
in_virtual_system(|mut env, _state| async move {
env.traps
.set_action(
&mut env.system,
SIGCHLD,
Action::Command("echo foo".into()),
Location::dummy(""),
false,
)
.unwrap();
let subshell = Subshell::new(|env, _job_control| {
Box::pin(async {
let trap_state = assert_matches!(
env.traps.get_state(SIGCHLD),
(None, Some(trap_state)) => trap_state
);
assert_matches!(
&trap_state.action,
Action::Command(body) => assert_eq!(&**body, "echo foo")
);
})
});
let pid = subshell.start(&mut env).await.unwrap().0;
env.wait_for_subshell(pid).await.unwrap();
});
}
#[test]
fn subshell_with_no_job_control() {
in_virtual_system(|mut parent_env, state| async move {
parent_env.options.set(Monitor, On);
let parent_pgid = state.borrow().processes[&parent_env.main_pid].pgid;
let state_2 = Rc::clone(&state);
let (child_pid, job_control) = Subshell::new(move |child_env, job_control| {
Box::pin(async move {
let child_pid = child_env.system.getpid();
assert_eq!(state_2.borrow().processes[&child_pid].pgid, parent_pgid);
assert_eq!(state_2.borrow().foreground, None);
assert_eq!(job_control, None);
})
})
.job_control(None)
.start(&mut parent_env)
.await
.unwrap();
assert_eq!(job_control, None);
assert_eq!(state.borrow().processes[&child_pid].pgid, parent_pgid);
assert_eq!(state.borrow().foreground, None);
parent_env.wait_for_subshell(child_pid).await.unwrap();
assert_eq!(state.borrow().processes[&child_pid].pgid, parent_pgid);
assert_eq!(state.borrow().foreground, None);
});
}
#[test]
fn subshell_in_background() {
in_virtual_system(|mut parent_env, state| async move {
parent_env.options.set(Monitor, On);
let state_2 = Rc::clone(&state);
let (child_pid, job_control) = Subshell::new(move |child_env, job_control| {
Box::pin(async move {
let child_pid = child_env.system.getpid();
assert_eq!(state_2.borrow().processes[&child_pid].pgid, child_pid);
assert_eq!(state_2.borrow().foreground, None);
assert_eq!(job_control, Some(JobControl::Background));
})
})
.job_control(JobControl::Background)
.start(&mut parent_env)
.await
.unwrap();
assert_eq!(job_control, Some(JobControl::Background));
assert_eq!(state.borrow().processes[&child_pid].pgid, child_pid);
assert_eq!(state.borrow().foreground, None);
parent_env.wait_for_subshell(child_pid).await.unwrap();
assert_eq!(state.borrow().processes[&child_pid].pgid, child_pid);
assert_eq!(state.borrow().foreground, None);
});
}
#[test]
fn subshell_in_foreground() {
in_virtual_system(|mut parent_env, state| async move {
parent_env.options.set(Monitor, On);
stub_tty(&state);
let state_2 = Rc::clone(&state);
let (child_pid, job_control) = Subshell::new(move |child_env, job_control| {
Box::pin(async move {
let child_pid = child_env.system.getpid();
assert_eq!(state_2.borrow().processes[&child_pid].pgid, child_pid);
assert_eq!(state_2.borrow().foreground, Some(child_pid));
assert_eq!(job_control, Some(JobControl::Foreground));
})
})
.job_control(JobControl::Foreground)
.start(&mut parent_env)
.await
.unwrap();
assert_eq!(job_control, Some(JobControl::Foreground));
assert_eq!(state.borrow().processes[&child_pid].pgid, child_pid);
parent_env.wait_for_subshell(child_pid).await.unwrap();
assert_eq!(state.borrow().processes[&child_pid].pgid, child_pid);
assert_eq!(state.borrow().foreground, Some(child_pid));
});
}
#[test]
fn tty_after_starting_foreground_subshell() {
in_virtual_system(|mut parent_env, state| async move {
parent_env.options.set(Monitor, On);
stub_tty(&state);
let _ = Subshell::new(move |_, _| Box::pin(std::future::ready(())))
.job_control(JobControl::Foreground)
.start(&mut parent_env)
.await
.unwrap();
assert_matches!(parent_env.tty, Some(_));
});
}
#[test]
fn no_job_control_with_option_disabled() {
in_virtual_system(|mut parent_env, state| async move {
stub_tty(&state);
let parent_pgid = state.borrow().processes[&parent_env.main_pid].pgid;
let state_2 = Rc::clone(&state);
let (child_pid, job_control) = Subshell::new(move |child_env, _job_control| {
Box::pin(async move {
let child_pid = child_env.system.getpid();
assert_eq!(state_2.borrow().processes[&child_pid].pgid, parent_pgid);
assert_eq!(state_2.borrow().foreground, None);
})
})
.job_control(JobControl::Foreground)
.start(&mut parent_env)
.await
.unwrap();
assert_eq!(job_control, None);
assert_eq!(state.borrow().processes[&child_pid].pgid, parent_pgid);
assert_eq!(state.borrow().foreground, None);
parent_env.wait_for_subshell(child_pid).await.unwrap();
assert_eq!(state.borrow().processes[&child_pid].pgid, parent_pgid);
assert_eq!(state.borrow().foreground, None);
});
}
#[test]
fn no_job_control_for_nested_subshell() {
in_virtual_system(|mut parent_env, state| async move {
let mut parent_env = parent_env.push_frame(Frame::Subshell);
parent_env.options.set(Monitor, On);
stub_tty(&state);
let parent_pgid = state.borrow().processes[&parent_env.main_pid].pgid;
let state_2 = Rc::clone(&state);
let (child_pid, job_control) = Subshell::new(move |child_env, _job_control| {
Box::pin(async move {
let child_pid = child_env.system.getpid();
assert_eq!(state_2.borrow().processes[&child_pid].pgid, parent_pgid);
assert_eq!(state_2.borrow().foreground, None);
})
})
.job_control(JobControl::Foreground)
.start(&mut parent_env)
.await
.unwrap();
assert_eq!(job_control, None);
assert_eq!(state.borrow().processes[&child_pid].pgid, parent_pgid);
assert_eq!(state.borrow().foreground, None);
parent_env.wait_for_subshell(child_pid).await.unwrap();
assert_eq!(state.borrow().processes[&child_pid].pgid, parent_pgid);
assert_eq!(state.borrow().foreground, None);
});
}
#[test]
fn wait_without_job_control() {
in_virtual_system(|mut env, _state| async move {
let subshell = Subshell::new(|env, _job_control| {
Box::pin(async { env.exit_status = ExitStatus(42) })
});
let (_pid, process_result) = subshell.start_and_wait(&mut env).await.unwrap();
assert_eq!(process_result, ProcessResult::exited(42));
});
}
#[test]
fn wait_for_foreground_job_to_exit() {
in_virtual_system(|mut env, state| async move {
env.options.set(Monitor, On);
stub_tty(&state);
let subshell = Subshell::new(|env, _job_control| {
Box::pin(async { env.exit_status = ExitStatus(123) })
})
.job_control(JobControl::Foreground);
let (_pid, process_result) = subshell.start_and_wait(&mut env).await.unwrap();
assert_eq!(process_result, ProcessResult::exited(123));
assert_eq!(state.borrow().foreground, Some(env.main_pgid));
});
}
#[test]
fn sigint_sigquit_not_ignored_by_default() {
in_virtual_system(|mut parent_env, state| async move {
let (child_pid, _) = Subshell::new(|env, _job_control| {
Box::pin(async { env.exit_status = ExitStatus(123) })
})
.job_control(JobControl::Background)
.start(&mut parent_env)
.await
.unwrap();
parent_env.wait_for_subshell(child_pid).await.unwrap();
let state = state.borrow();
let process = &state.processes[&child_pid];
assert_eq!(process.disposition(SIGINT), Disposition::Default);
assert_eq!(process.disposition(SIGQUIT), Disposition::Default);
})
}
#[test]
fn sigint_sigquit_ignored_in_uncontrolled_job() {
in_virtual_system(|mut parent_env, state| async move {
let (child_pid, _) = Subshell::new(|env, _job_control| {
Box::pin(async { env.exit_status = ExitStatus(123) })
})
.job_control(JobControl::Background)
.ignore_sigint_sigquit(true)
.start(&mut parent_env)
.await
.unwrap();
parent_env
.system
.kill(child_pid, Some(SIGINT))
.await
.unwrap();
parent_env
.system
.kill(child_pid, Some(SIGQUIT))
.await
.unwrap();
let child_result = parent_env.wait_for_subshell(child_pid).await.unwrap();
assert_eq!(child_result, (child_pid, ProcessState::exited(123)));
let state = state.borrow();
let parent_process = &state.processes[&parent_env.main_pid];
assert!(!parent_process.blocked_signals().contains(&SIGINT));
assert!(!parent_process.blocked_signals().contains(&SIGQUIT));
let child_process = &state.processes[&child_pid];
assert_eq!(child_process.disposition(SIGINT), Disposition::Ignore);
assert_eq!(child_process.disposition(SIGQUIT), Disposition::Ignore);
})
}
#[test]
fn sigint_sigquit_not_ignored_if_job_controlled() {
in_virtual_system(|mut parent_env, state| async move {
parent_env.options.set(Monitor, On);
stub_tty(&state);
let (child_pid, _) = Subshell::new(|env, _job_control| {
Box::pin(async { env.exit_status = ExitStatus(123) })
})
.job_control(JobControl::Background)
.ignore_sigint_sigquit(true)
.start(&mut parent_env)
.await
.unwrap();
parent_env.wait_for_subshell(child_pid).await.unwrap();
let state = state.borrow();
let process = &state.processes[&child_pid];
assert_eq!(process.disposition(SIGINT), Disposition::Default);
assert_eq!(process.disposition(SIGQUIT), Disposition::Default);
})
}
#[test]
fn internal_dispositions_for_stoppers_kept_in_uncontrolled_subshell_of_controlling_interactive_shell(
) {
in_virtual_system(|mut parent_env, state| async move {
parent_env.options.set(Interactive, On);
parent_env.options.set(Monitor, On);
parent_env
.traps
.enable_internal_dispositions_for_stoppers(&mut parent_env.system)
.unwrap();
stub_tty(&state);
let (child_pid, _) = Subshell::new(|env, _job_control| {
Box::pin(async { env.exit_status = ExitStatus(123) })
})
.start(&mut parent_env)
.await
.unwrap();
let child_result = parent_env.wait_for_subshell(child_pid).await.unwrap();
assert_eq!(child_result, (child_pid, ProcessState::exited(123)));
let state = state.borrow();
let child_process = &state.processes[&child_pid];
assert_eq!(child_process.disposition(SIGTSTP), Disposition::Ignore);
assert_eq!(child_process.disposition(SIGTTIN), Disposition::Ignore);
assert_eq!(child_process.disposition(SIGTTOU), Disposition::Ignore);
})
}
#[test]
fn internal_dispositions_for_stoppers_reset_in_controlled_subshell_of_interactive_shell() {
in_virtual_system(|mut parent_env, state| async move {
parent_env.options.set(Interactive, On);
parent_env.options.set(Monitor, On);
parent_env
.traps
.enable_internal_dispositions_for_stoppers(&mut parent_env.system)
.unwrap();
stub_tty(&state);
let (child_pid, _) = Subshell::new(|env, _job_control| {
Box::pin(async { env.exit_status = ExitStatus(123) })
})
.job_control(JobControl::Background)
.start(&mut parent_env)
.await
.unwrap();
let child_result = parent_env.wait_for_subshell(child_pid).await.unwrap();
assert_eq!(child_result, (child_pid, ProcessState::exited(123)));
let state = state.borrow();
let child_process = &state.processes[&child_pid];
assert_eq!(child_process.disposition(SIGTSTP), Disposition::Default);
assert_eq!(child_process.disposition(SIGTTIN), Disposition::Default);
assert_eq!(child_process.disposition(SIGTTOU), Disposition::Default);
})
}
#[test]
fn internal_dispositions_for_stoppers_unset_in_subshell_of_non_controlling_interactive_shell() {
in_virtual_system(|mut parent_env, state| async move {
parent_env.options.set(Interactive, On);
stub_tty(&state);
let (child_pid, _) = Subshell::new(|env, _job_control| {
Box::pin(async { env.exit_status = ExitStatus(123) })
})
.start(&mut parent_env)
.await
.unwrap();
let child_result = parent_env.wait_for_subshell(child_pid).await.unwrap();
assert_eq!(child_result, (child_pid, ProcessState::exited(123)));
let state = state.borrow();
let child_process = &state.processes[&child_pid];
assert_eq!(child_process.disposition(SIGTSTP), Disposition::Default);
assert_eq!(child_process.disposition(SIGTTIN), Disposition::Default);
assert_eq!(child_process.disposition(SIGTTOU), Disposition::Default);
})
}
#[test]
fn internal_dispositions_for_stoppers_unset_in_uncontrolled_subshell_of_controlling_non_interactive_shell(
) {
in_virtual_system(|mut parent_env, state| async move {
parent_env.options.set(Monitor, On);
stub_tty(&state);
let (child_pid, _) = Subshell::new(|env, _job_control| {
Box::pin(async { env.exit_status = ExitStatus(123) })
})
.start(&mut parent_env)
.await
.unwrap();
let child_result = parent_env.wait_for_subshell(child_pid).await.unwrap();
assert_eq!(child_result, (child_pid, ProcessState::exited(123)));
let state = state.borrow();
let child_process = &state.processes[&child_pid];
assert_eq!(child_process.disposition(SIGTSTP), Disposition::Default);
assert_eq!(child_process.disposition(SIGTTIN), Disposition::Default);
assert_eq!(child_process.disposition(SIGTTOU), Disposition::Default);
})
}
}