use crate::bg::OperandErrorKind;
use crate::bg::ResumeError;
use crate::common::report_error;
use crate::common::report_simple_failure;
use crate::common::syntax::parse_arguments;
use crate::common::syntax::Mode;
use yash_env::io::Fd;
use yash_env::job::id::parse;
#[cfg(doc)]
use yash_env::job::JobList;
use yash_env::job::Pid;
use yash_env::job::ProcessState;
use yash_env::semantics::ExitStatus;
use yash_env::semantics::Field;
use yash_env::system::Errno;
use yash_env::system::System as _;
use yash_env::system::SystemEx as _;
use yash_env::trap::Signal;
use yash_env::Env;
async fn wait_while_running(env: &mut Env, pid: Pid) -> Result<ProcessState, Errno> {
loop {
let (_pid, state) = env.wait_for_subshell(pid).await?;
match state {
ProcessState::Running => (),
ProcessState::Stopped(_) | ProcessState::Exited(_) | ProcessState::Signaled { .. } => {
return Ok(state)
}
}
}
}
async fn resume_job_by_index(env: &mut Env, index: usize) -> Result<ProcessState, ResumeError> {
let tty = env.get_tty()?;
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 mut state = job.state;
if state.is_alive() {
env.system.tcsetpgrp_without_block(tty, job.pid)?;
let pgid = -job.pid;
env.system.kill(pgid, Signal::SIGCONT.into()).await?;
state = wait_while_running(env, job.pid).await?;
env.system.tcsetpgrp_with_block(tty, env.main_pgid)?;
}
if !state.is_alive() {
env.jobs.remove(index);
}
Ok(state)
}
async fn resume_job_by_id(env: &mut Env, job_id: &str) -> Result<ProcessState, 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, []);
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(state) => ExitStatus::try_from(state).unwrap().into(),
Err(error) => report_simple_failure(env, &error.to_string()).await,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::assert_stderr;
use crate::tests::assert_stdout;
use crate::tests::in_virtual_system;
use crate::tests::stub_tty;
use futures_util::FutureExt as _;
use std::cell::Cell;
use std::ops::ControlFlow::Continue;
use std::rc::Rc;
use yash_env::job::Job;
use yash_env::job::ProcessState;
use yash_env::option::Option::Monitor;
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::VirtualSystem;
async fn suspend(env: &mut Env) {
env.system
.kill(env.system.getpid(), Some(Signal::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);
Continue(())
})
})
.job_control(JobControl::Foreground);
let (pid, subshell_state) = subshell.start_and_wait(&mut env).await.unwrap();
assert_eq!(subshell_state, ProcessState::Stopped(Signal::SIGSTOP));
let mut job = Job::new(pid);
job.job_controlled = true;
job.state = subshell_state;
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 {
stub_tty(&state);
env.options.set(Monitor, On);
let subshell = Subshell::new(|env, _| {
Box::pin(async move {
suspend(env).await;
Continue(())
})
})
.job_control(JobControl::Foreground);
let (pid, subshell_state) = subshell.start_and_wait(&mut env).await.unwrap();
assert_eq!(subshell_state, ProcessState::Stopped(Signal::SIGSTOP));
let mut job = Job::new(pid);
job.job_controlled = true;
job.state = subshell_state;
job.name = "my job name".to_owned();
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 {
stub_tty(&state);
env.options.set(Monitor, On);
let subshell = Subshell::new(|env, _| {
Box::pin(async move {
suspend(env).await;
env.exit_status = ExitStatus(42);
Continue(())
})
})
.job_control(JobControl::Foreground);
let (pid, subshell_state) = subshell.start_and_wait(&mut env).await.unwrap();
assert_eq!(subshell_state, ProcessState::Stopped(Signal::SIGSTOP));
let mut job = Job::new(pid);
job.job_controlled = true;
job.state = subshell_state;
let index = env.jobs.add(job);
let result = resume_job_by_index(&mut env, index).await.unwrap();
assert_eq!(result, ProcessState::Exited(ExitStatus(42)));
let state = state.borrow().processes[&pid].state();
assert_eq!(state, ProcessState::Exited(ExitStatus(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 {
stub_tty(&state);
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_state) = subshell.start_and_wait(&mut env).await.unwrap();
assert_eq!(subshell_state, ProcessState::Stopped(Signal::SIGSTOP));
let mut job = Job::new(pid);
job.job_controlled = true;
job.state = subshell_state;
let index = env.jobs.add(job);
let result = resume_job_by_index(&mut env, index).await.unwrap();
assert_eq!(result, ProcessState::Stopped(Signal::SIGSTOP));
let job_state = env.jobs[index].state;
assert_eq!(job_state, ProcessState::Stopped(Signal::SIGSTOP));
let state = state.borrow().processes[&pid].state();
assert_eq!(state, ProcessState::Stopped(Signal::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(async move {
suspend(env).await;
Continue(())
})
})
.job_control(JobControl::Foreground);
let (pid, subshell_state) = subshell.start_and_wait(&mut env).await.unwrap();
assert_eq!(subshell_state, ProcessState::Stopped(Signal::SIGSTOP));
let mut job = Job::new(pid);
job.job_controlled = true;
job.state = subshell_state;
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();
stub_tty(&system.state);
let mut env = Env::with_system(Box::new(system.clone()));
let pid = Pid(123);
let mut job = Job::new(pid);
job.job_controlled = true;
job.state = ProcessState::Exited(ExitStatus(12));
let index = env.jobs.add(job);
let mut process = Process::with_parent_and_group(system.process_id, pid);
_ = process.set_state(ProcessState::Stopped(Signal::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(ProcessState::Exited(ExitStatus(12))));
assert_eq!(env.jobs.get(index), None);
let state = system.state.borrow();
assert_eq!(
state.processes[&pid].state(),
ProcessState::Stopped(Signal::SIGSTOP),
);
}
#[test]
fn resume_job_by_index_rejects_unowned_job() {
let system = VirtualSystem::new();
stub_tty(&system.state);
let mut env = Env::with_system(Box::new(system));
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();
stub_tty(&system.state);
let mut env = Env::with_system(Box::new(system));
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_state1) = subshell.start_and_wait(&mut env).await.unwrap();
assert_eq!(subshell_state1, ProcessState::Stopped(Signal::SIGSTOP));
let mut job = Job::new(pid1);
job.job_controlled = true;
job.state = subshell_state1;
env.jobs.add(job);
let subshell = Subshell::new(|env, _| {
Box::pin(async move {
suspend(env).await;
Continue(())
})
})
.job_control(JobControl::Foreground);
let (pid2, subshell_state2) = subshell.start_and_wait(&mut env).await.unwrap();
assert_eq!(subshell_state2, ProcessState::Stopped(Signal::SIGSTOP));
let mut job = Job::new(pid2);
job.job_controlled = true;
job.state = subshell_state2;
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(Signal::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()));
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 {
stub_tty(&state);
env.options.set(Monitor, On);
let subshell = Subshell::new(|env, _| {
Box::pin(async move {
suspend(env).await;
Continue(())
})
})
.job_control(JobControl::Foreground);
let (pid1, subshell_state1) = subshell.start_and_wait(&mut env).await.unwrap();
assert_eq!(subshell_state1, ProcessState::Stopped(Signal::SIGSTOP));
let mut job = Job::new(pid1);
job.job_controlled = true;
job.state = subshell_state1;
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_state2) = subshell.start_and_wait(&mut env).await.unwrap();
assert_eq!(subshell_state2, ProcessState::Stopped(Signal::SIGSTOP));
let mut job = Job::new(pid2);
job.job_controlled = true;
job.state = subshell_state2;
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(Signal::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()));
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:?}");
});
}
}