use crate::Env;
use crate::job::Pid;
use crate::job::ProcessResult;
use crate::job::RunBlocking;
use crate::job::RunUnblocking;
use crate::system::Close;
use crate::system::Dup;
use crate::system::Errno;
use crate::system::Exit;
use crate::system::Fork;
use crate::system::GetPid;
use crate::system::Open;
use crate::system::SendSignal;
use crate::system::SetPgid;
use crate::system::Sigmask;
use crate::system::SigmaskOp;
use crate::system::Signals;
use crate::system::Sigset as _;
use crate::system::TcSetPgrp;
use crate::system::Wait;
use crate::system::concurrency::WaitForSignals;
use crate::system::resource::SetRlimit;
use crate::trap::SignalSystem;
use std::marker::PhantomData;
use std::pin::Pin;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum JobControl {
Foreground,
Background,
}
#[deprecated(
note = "use `Config` for subshell configuration instead",
since = "0.14.0"
)]
#[must_use = "a subshell is not started unless you call `Subshell::start`"]
pub struct Subshell<S, F> {
task: F,
job_control: Option<JobControl>,
ignores_sigint_sigquit: bool,
phantom_data: PhantomData<fn(&mut Env<S>)>,
}
#[allow(deprecated, reason = "for backward compatible API")]
impl<S, F> std::fmt::Debug for Subshell<S, F> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Subshell")
.field("job_control", &self.job_control)
.field("ignores_sigint_sigquit", &self.ignores_sigint_sigquit)
.finish_non_exhaustive()
}
}
#[allow(deprecated, reason = "for backward compatible API")]
impl<S, F> Subshell<S, F>
where
S: BlockSignals
+ Close
+ Dup
+ Exit
+ Fork
+ GetPid
+ Open
+ RunBlocking
+ RunUnblocking
+ SendSignal
+ SetPgid
+ SetRlimit
+ SignalSystem
+ TcSetPgrp
+ WaitForSignals
+ 'static,
F: for<'a> FnOnce(&'a mut Env<S>, Option<JobControl>) -> Pin<Box<dyn Future<Output = ()> + 'a>>
+ 'static,
{
pub fn new(task: F) -> Self {
Subshell {
task,
job_control: None,
ignores_sigint_sigquit: false,
phantom_data: PhantomData,
}
}
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
}
fn into_config_and_task(self) -> (Config, F) {
let config = Config {
job_control: self.job_control,
ignores_sigint_sigquit: self.ignores_sigint_sigquit,
};
(config, self.task)
}
pub async fn start(self, env: &mut Env<S>) -> Result<(Pid, Option<JobControl>), Errno> {
let (config, task) = self.into_config_and_task();
config
.start(env, async move |env, job_control| {
task(env, job_control).await
})
.await
}
pub async fn start_and_wait(self, env: &mut Env<S>) -> Result<(Pid, ProcessResult), Errno>
where
S: Wait,
{
let (config, task) = self.into_config_and_task();
config
.start_and_wait(env, async move |env, job_control| {
task(env, job_control).await
})
.await
}
}
pub trait BlockSignals: Signals {
type SavedMask;
fn block_sigint_sigquit(
&self,
) -> impl Future<Output = Result<Self::SavedMask, Errno>> + use<'_, Self>;
fn restore_sigmask(
&self,
mask: Self::SavedMask,
) -> impl Future<Output = Result<(), Errno>> + use<'_, Self>;
}
impl<S> BlockSignals for S
where
S: Sigmask + ?Sized,
{
type SavedMask = S::Sigset;
async fn block_sigint_sigquit(&self) -> Result<Self::SavedMask, Errno> {
let mut old_mask = S::Sigset::new();
self.sigmask(
Some((
SigmaskOp::Add,
&S::Sigset::from_signals([S::SIGINT, S::SIGQUIT])?,
)),
Some(&mut old_mask),
)
.await?;
Ok(old_mask)
}
async fn restore_sigmask(&self, mask: Self::SavedMask) -> Result<(), Errno> {
self.sigmask(Some((SigmaskOp::Set, &mask)), None).await
}
}
mod config;
pub use config::Config;
#[allow(deprecated, reason = "for backward compatible API")]
#[cfg(test)]
mod tests {
use super::*;
use crate::job::{Job, ProcessState};
use crate::option::Option::{Interactive, Monitor};
use crate::option::State::On;
use crate::semantics::ExitStatus;
use crate::source::Location;
use crate::stack::Frame;
use crate::system::r#virtual::{Inode, SystemState, VirtualSystem};
use crate::system::r#virtual::{SIGCHLD, SIGINT, SIGQUIT, SIGTSTP, SIGTTIN, SIGTTOU};
use crate::system::{Concurrent, Disposition};
use crate::test_helper::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;
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: &mut Env<Rc<Concurrent<VirtualSystem>>>, _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(
&env.system,
SIGCHLD,
Action::Command("echo foo".into()),
Location::dummy(""),
false,
)
.await
.unwrap();
let subshell = Subshell::new(|env, _job_control| {
Box::pin(async {
let (current, parent) = env.traps.get_state(SIGCHLD);
assert_eq!(current.unwrap().action, Action::Default);
assert_matches!(
&parent.unwrap().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: &mut Env<Rc<Concurrent<VirtualSystem>>>, 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: &mut Env<Rc<Concurrent<VirtualSystem>>>, 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: &mut Env<Rc<Concurrent<VirtualSystem>>>, 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 job_control_without_tty() {
in_virtual_system(async |mut parent_env, state| {
parent_env.options.set(Monitor, On);
let state_2 = Rc::clone(&state);
let (child_pid, job_control) = Subshell::new(
move |child_env: &mut Env<Rc<Concurrent<VirtualSystem>>>, 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!(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);
})
}
#[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: &mut Env<Rc<Concurrent<VirtualSystem>>>, _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: &mut Env<Rc<Concurrent<VirtualSystem>>>, _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_eq!(parent_process.blocked_signals().contains(SIGINT), Ok(false));
assert_eq!(
parent_process.blocked_signals().contains(SIGQUIT),
Ok(false)
);
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(&parent_env.system)
.await
.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(&parent_env.system)
.await
.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);
})
}
}