use crate::bg::OperandErrorKind;
use crate::bg::ResumeError;
use crate::common::report::{report_error, report_simple_failure};
use crate::common::syntax::Mode;
use crate::common::syntax::parse_arguments;
use std::ops::ControlFlow::Break;
use yash_env::Env;
use yash_env::io::Fd;
#[cfg(doc)]
use yash_env::job::JobList;
use yash_env::job::ProcessResult;
use yash_env::job::ProcessState;
use yash_env::job::id::parse;
use yash_env::option::Option::Monitor;
use yash_env::option::State::Off;
use yash_env::semantics::Divert::Interrupt;
use yash_env::semantics::ExitStatus;
use yash_env::semantics::Field;
use yash_env::signal;
use yash_env::system::Errno;
use yash_env::system::System as _;
use yash_env::system::SystemEx as _;
async fn resume_job_by_index(env: &mut Env, index: usize) -> Result<ProcessResult, ResumeError> {
let tty = env.get_tty().ok();
let job = &env.jobs[index];
if !job.is_owned {
return Err(ResumeError::Unowned);
}
if !job.job_controlled {
return Err(ResumeError::Unmonitored);
}
let line = format!("{}\n", job.name);
env.system.write_all(Fd::STDOUT, line.as_bytes()).await?;
drop(line);
let result = match job.state {
ProcessState::Halted(
result @ (ProcessResult::Exited(_) | ProcessResult::Signaled { .. }),
) => result,
ProcessState::Halted(ProcessResult::Stopped(_)) | ProcessState::Running => {
if let Some(tty) = tty {
env.system.tcsetpgrp(tty, job.pid)?;
}
let pgid = -job.pid;
let sigcont = env.system.signal_number_from_name(signal::Name::Cont);
let sigcont = sigcont.ok_or(Errno::EINVAL)?;
env.system.kill(pgid, Some(sigcont)).await?;
let result = env.wait_for_subshell_to_halt(job.pid).await?.1;
if let Some(tty) = tty {
env.system.tcsetpgrp_with_block(tty, env.main_pgid)?;
}
result
}
};
if !result.is_stopped() {
env.jobs.remove(index);
}
Ok(result)
}
async fn resume_job_by_id(env: &mut Env, job_id: &str) -> Result<ProcessResult, OperandErrorKind> {
let job_id = parse(job_id)?;
let index = job_id.find(&env.jobs)?;
Ok(resume_job_by_index(env, index).await?)
}
pub async fn main(env: &mut Env, args: Vec<Field>) -> crate::Result {
let (options, operands) = match parse_arguments(&[], Mode::with_env(env), args) {
Ok(result) => result,
Err(error) => return report_error(env, &error).await,
};
debug_assert_eq!(options, []);
if env.options.get(Monitor) == Off {
return report_simple_failure(env, "job control is disabled").await;
}
let result = if operands.is_empty() {
if let Some(index) = env.jobs.current_job() {
resume_job_by_index(env, index).await.map_err(Into::into)
} else {
return report_simple_failure(env, "there is no job").await;
}
} else if operands.len() > 1 {
return report_simple_failure(env, "too many operands").await;
} else {
resume_job_by_id(env, &operands[0].value).await
};
match result {
Ok(result) if env.is_interactive() => {
let divert = Break(Interrupt(Some(ExitStatus::from(result))));
crate::Result::with_exit_status_and_divert(env.exit_status, divert)
}
Ok(result) => ExitStatus::from(result).into(),
Err(error) => report_simple_failure(env, &error.to_string()).await,
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures_util::FutureExt as _;
use std::cell::Cell;
use std::rc::Rc;
use yash_env::VirtualSystem;
use yash_env::job::Job;
use yash_env::job::Pid;
use yash_env::job::ProcessResult;
use yash_env::option::Option::Interactive;
use yash_env::option::State::On;
use yash_env::subshell::JobControl;
use yash_env::subshell::Subshell;
use yash_env::system::r#virtual::Process;
use yash_env::system::r#virtual::SIGSTOP;
use yash_env_test_helper::assert_stderr;
use yash_env_test_helper::assert_stdout;
use yash_env_test_helper::in_virtual_system;
use yash_env_test_helper::stub_tty;
async fn suspend(env: &mut Env) {
let target = env.system.getpid();
env.system.kill(target, Some(SIGSTOP)).await.unwrap();
}
#[test]
fn resume_job_by_index_resumes_job_in_foreground() {
in_virtual_system(|mut env, state| async move {
stub_tty(&state);
env.options.set(Monitor, On);
let reached = Rc::new(Cell::new(false));
let reached2 = Rc::clone(&reached);
let subshell = Subshell::new(|env, _| {
Box::pin(async move {
suspend(env).await;
let tty = env.get_tty().unwrap();
assert_eq!(env.system.tcgetpgrp(tty).unwrap(), env.system.getpid());
reached2.set(true);
})
})
.job_control(JobControl::Foreground);
let (pid, subshell_result) = subshell.start_and_wait(&mut env).await.unwrap();
assert_eq!(subshell_result, ProcessResult::Stopped(SIGSTOP));
let mut job = Job::new(pid);
job.job_controlled = true;
job.state = subshell_result.into();
let index = env.jobs.add(job);
resume_job_by_index(&mut env, index).await.unwrap();
assert!(reached.get());
})
}
#[test]
fn resume_job_by_index_resume_job_even_without_tty() {
in_virtual_system(async |mut env, _| {
env.options.set(Monitor, On);
let reached = Rc::new(Cell::new(false));
let reached2 = Rc::clone(&reached);
let subshell = Subshell::new(|env, _| {
Box::pin(async move {
suspend(env).await;
reached2.set(true);
})
})
.job_control(JobControl::Foreground);
let (pid, subshell_result) = subshell.start_and_wait(&mut env).await.unwrap();
assert_eq!(subshell_result, ProcessResult::Stopped(SIGSTOP));
let mut job = Job::new(pid);
job.job_controlled = true;
job.state = subshell_result.into();
let index = env.jobs.add(job);
resume_job_by_index(&mut env, index).await.unwrap();
assert!(reached.get());
})
}
#[test]
fn resume_job_by_index_prints_job_name() {
in_virtual_system(|mut env, state| async move {
env.options.set(Monitor, On);
let subshell =
Subshell::new(|env, _| Box::pin(suspend(env))).job_control(JobControl::Foreground);
let (pid, subshell_result) = subshell.start_and_wait(&mut env).await.unwrap();
assert_eq!(subshell_result, ProcessResult::Stopped(SIGSTOP));
let mut job = Job::new(pid);
job.job_controlled = true;
job.state = subshell_result.into();
"my job name".clone_into(&mut job.name);
let index = env.jobs.add(job);
resume_job_by_index(&mut env, index).await.unwrap();
assert_stdout(&state, |stdout| assert_eq!(stdout, "my job name\n"));
})
}
#[test]
fn resume_job_by_index_returns_after_job_exits() {
in_virtual_system(|mut env, state| async move {
env.options.set(Monitor, On);
let subshell = Subshell::new(|env, _| {
Box::pin(async move {
suspend(env).await;
env.exit_status = ExitStatus(42);
})
})
.job_control(JobControl::Foreground);
let (pid, subshell_result) = subshell.start_and_wait(&mut env).await.unwrap();
assert_eq!(subshell_result, ProcessResult::Stopped(SIGSTOP));
let mut job = Job::new(pid);
job.job_controlled = true;
job.state = subshell_result.into();
let index = env.jobs.add(job);
let result = resume_job_by_index(&mut env, index).await.unwrap();
assert_eq!(result, ProcessResult::exited(42));
let state = state.borrow().processes[&pid].state();
assert_eq!(state, ProcessState::exited(42));
assert_eq!(env.jobs.get(index), None);
})
}
#[test]
fn resume_job_by_index_returns_after_job_suspends() {
in_virtual_system(|mut env, state| async move {
env.options.set(Monitor, On);
let subshell = Subshell::new(|env, _| {
Box::pin(async move {
suspend(env).await;
suspend(env).await;
unreachable!("child process should not be resumed twice");
})
})
.job_control(JobControl::Foreground);
let (pid, subshell_result) = subshell.start_and_wait(&mut env).await.unwrap();
assert_eq!(subshell_result, ProcessResult::Stopped(SIGSTOP));
let mut job = Job::new(pid);
job.job_controlled = true;
job.state = subshell_result.into();
let index = env.jobs.add(job);
let result = resume_job_by_index(&mut env, index).await.unwrap();
assert_eq!(result, ProcessResult::Stopped(SIGSTOP));
let job_state = env.jobs[index].state;
assert_eq!(job_state, ProcessState::stopped(SIGSTOP));
let state = state.borrow().processes[&pid].state();
assert_eq!(state, ProcessState::stopped(SIGSTOP));
})
}
#[test]
fn resume_job_by_index_moves_shell_back_to_foreground() {
in_virtual_system(|mut env, state| async move {
stub_tty(&state);
env.options.set(Monitor, On);
let subshell =
Subshell::new(|env, _| Box::pin(suspend(env))).job_control(JobControl::Foreground);
let (pid, subshell_result) = subshell.start_and_wait(&mut env).await.unwrap();
assert_eq!(subshell_result, ProcessResult::Stopped(SIGSTOP));
let mut job = Job::new(pid);
job.job_controlled = true;
job.state = subshell_result.into();
let index = env.jobs.add(job);
_ = resume_job_by_index(&mut env, index).await.unwrap();
let foreground = state.borrow().foreground;
assert_eq!(foreground, Some(env.main_pgid));
})
}
#[test]
fn resume_job_by_index_sends_no_sigcont_to_dead_process() {
let system = VirtualSystem::new();
let mut env = Env::with_system(Box::new(system.clone()));
env.options.set(Monitor, On);
let pid = Pid(123);
let mut job = Job::new(pid);
job.job_controlled = true;
job.state = ProcessState::exited(12);
let index = env.jobs.add(job);
let mut process = Process::with_parent_and_group(system.process_id, pid);
_ = process.set_state(ProcessState::stopped(SIGSTOP));
{
let mut state = system.state.borrow_mut();
state.processes.insert(pid, process);
}
let result = resume_job_by_index(&mut env, index).now_or_never().unwrap();
assert_eq!(result, Ok(ProcessResult::exited(12)));
assert_eq!(env.jobs.get(index), None);
let state = system.state.borrow();
assert_eq!(
state.processes[&pid].state(),
ProcessState::stopped(SIGSTOP),
);
}
#[test]
fn resume_job_by_index_rejects_unowned_job() {
let system = VirtualSystem::new();
let mut env = Env::with_system(Box::new(system));
env.options.set(Monitor, On);
let mut job = Job::new(Pid(123));
job.job_controlled = true;
job.is_owned = false;
let index = env.jobs.add(job);
let result = resume_job_by_index(&mut env, index).now_or_never().unwrap();
assert_eq!(result, Err(ResumeError::Unowned));
}
#[test]
fn resume_job_by_index_rejects_unmonitored_job() {
let system = VirtualSystem::new();
let mut env = Env::with_system(Box::new(system));
env.options.set(Monitor, On);
let index = env.jobs.add(Job::new(Pid(123)));
let result = resume_job_by_index(&mut env, index).now_or_never().unwrap();
assert_eq!(result, Err(ResumeError::Unmonitored));
}
#[test]
fn main_without_operands_resumes_current_job() {
in_virtual_system(|mut env, state| async move {
stub_tty(&state);
env.options.set(Monitor, On);
let subshell = Subshell::new(|env, _| {
Box::pin(async move {
suspend(env).await;
unreachable!("previous job should not be resumed");
})
})
.job_control(JobControl::Foreground);
let (pid1, subshell_result_1) = subshell.start_and_wait(&mut env).await.unwrap();
assert_eq!(subshell_result_1, ProcessResult::Stopped(SIGSTOP));
let mut job = Job::new(pid1);
job.job_controlled = true;
job.state = subshell_result_1.into();
env.jobs.add(job);
let subshell =
Subshell::new(|env, _| Box::pin(suspend(env))).job_control(JobControl::Foreground);
let (pid2, subshell_result_2) = subshell.start_and_wait(&mut env).await.unwrap();
assert_eq!(subshell_result_2, ProcessResult::Stopped(SIGSTOP));
let mut job = Job::new(pid2);
job.job_controlled = true;
job.state = subshell_result_2.into();
let index2 = env.jobs.add(job);
env.jobs.set_current_job(index2).unwrap();
let result = main(&mut env, vec![]).await;
assert_eq!(result, crate::Result::default());
assert_eq!(env.jobs.get(index2), None);
let state = state.borrow().processes[&pid1].state();
assert_eq!(state, ProcessState::stopped(SIGSTOP));
})
}
#[test]
fn main_without_operands_fails_if_there_is_no_current_job() {
let system = VirtualSystem::new();
let mut env = Env::with_system(Box::new(system.clone()));
env.options.set(Monitor, On);
let result = main(&mut env, vec![]).now_or_never().unwrap();
assert_eq!(result, crate::Result::from(ExitStatus::FAILURE));
assert_stderr(&system.state, |stderr| {
assert!(stderr.contains("there is no job"), "{stderr:?}");
});
}
#[test]
fn main_with_operand_resumes_specified_job() {
in_virtual_system(|mut env, state| async move {
env.options.set(Monitor, On);
let subshell =
Subshell::new(|env, _| Box::pin(suspend(env))).job_control(JobControl::Foreground);
let (pid1, subshell_result_1) = subshell.start_and_wait(&mut env).await.unwrap();
assert_eq!(subshell_result_1, ProcessResult::Stopped(SIGSTOP));
let mut job = Job::new(pid1);
job.job_controlled = true;
job.state = subshell_result_1.into();
job.name = "previous job".to_string();
let index1 = env.jobs.add(job);
let subshell = Subshell::new(|env, _| {
Box::pin(async move {
suspend(env).await;
unreachable!("current job should not be resumed");
})
})
.job_control(JobControl::Foreground);
let (pid2, subshell_result_2) = subshell.start_and_wait(&mut env).await.unwrap();
assert_eq!(subshell_result_2, ProcessResult::Stopped(SIGSTOP));
let mut job = Job::new(pid2);
job.job_controlled = true;
job.state = subshell_result_2.into();
let index2 = env.jobs.add(job);
env.jobs.set_current_job(index2).unwrap();
let result = main(&mut env, Field::dummies(["%prev"])).await;
assert_eq!(result, crate::Result::default());
assert_eq!(env.jobs.get(index1), None);
let state = state.borrow().processes[&pid2].state();
assert_eq!(state, ProcessState::stopped(SIGSTOP));
})
}
#[test]
fn main_with_operand_fails_if_jobs_is_not_found() {
let system = VirtualSystem::new();
let mut env = Env::with_system(Box::new(system.clone()));
env.options.set(Monitor, On);
let mut job = Job::new(Pid(123));
job.job_controlled = true;
job.name = "foo".to_string();
env.jobs.add(job);
let result = main(&mut env, Field::dummies(["%bar"]))
.now_or_never()
.unwrap();
assert_eq!(result, crate::Result::from(ExitStatus::FAILURE));
assert_stderr(&system.state, |stderr| {
assert!(stderr.contains("not found"), "{stderr:?}");
});
}
#[test]
fn main_returns_exit_status_if_job_suspends_if_not_interactive() {
in_virtual_system(|mut env, _| async move {
env.options.set(Monitor, On);
env.exit_status = ExitStatus(42);
let subshell = Subshell::new(|env, _| {
Box::pin(async move {
suspend(env).await;
suspend(env).await;
unreachable!("child process should not be resumed twice");
})
})
.job_control(JobControl::Foreground);
let (pid, subshell_result) = subshell.start_and_wait(&mut env).await.unwrap();
assert_eq!(subshell_result, ProcessResult::Stopped(SIGSTOP));
let mut job = Job::new(pid);
job.job_controlled = true;
job.state = subshell_result.into();
let index = env.jobs.add(job);
env.jobs.set_current_job(index).unwrap();
let result = main(&mut env, vec![]).await;
assert_eq!(result, crate::Result::from(ExitStatus::from(SIGSTOP)));
})
}
#[test]
fn main_returns_interrupt_if_job_suspends_if_interactive() {
in_virtual_system(|mut env, _| async move {
env.options.set(Interactive, On);
env.options.set(Monitor, On);
env.exit_status = ExitStatus(42);
let subshell = Subshell::new(|env, _| {
Box::pin(async move {
suspend(env).await;
suspend(env).await;
unreachable!("child process should not be resumed twice");
})
})
.job_control(JobControl::Foreground);
let (pid, subshell_result) = subshell.start_and_wait(&mut env).await.unwrap();
assert_eq!(subshell_result, ProcessResult::Stopped(SIGSTOP));
let mut job = Job::new(pid);
job.job_controlled = true;
job.state = subshell_result.into();
let index = env.jobs.add(job);
env.jobs.set_current_job(index).unwrap();
let result = main(&mut env, vec![]).await;
assert_eq!(
result,
crate::Result::with_exit_status_and_divert(
ExitStatus(42),
Break(Interrupt(Some(ExitStatus::from(SIGSTOP))))
)
);
})
}
}