use super::{Context, Input, Result};
use crate::Env;
use crate::io::Fd;
use crate::option::{IgnoreEof as IgnoreEofOption, Interactive, Off, On, PosixlyCorrect};
use crate::system::Isatty;
use crate::system::concurrency::WriteAll;
use std::cell::RefCell;
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct SuspendedJobsGuardConfig {
pub message: String,
}
impl SuspendedJobsGuardConfig {
#[must_use]
pub fn with_message<M: Into<String>>(message: M) -> Self {
Self {
message: message.into(),
}
}
}
#[derive(Clone, Debug, Default)]
#[non_exhaustive]
pub struct IgnoreEofConfig {
pub message: String,
}
impl IgnoreEofConfig {
#[must_use]
pub fn with_message<M: Into<String>>(message: M) -> Self {
Self {
message: message.into(),
}
}
}
#[derive(Debug)]
pub struct EofGuard<'a, 'b, S, T> {
inner: T,
fd: Fd,
env: &'a RefCell<&'b mut Env<S>>,
}
impl<'a, 'b, S, T> EofGuard<'a, 'b, S, T> {
pub fn new(inner: T, fd: Fd, env: &'a RefCell<&'b mut Env<S>>) -> Self {
Self { inner, fd, env }
}
}
impl<S, T: Clone> Clone for EofGuard<'_, '_, S, T> {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
fd: self.fd,
env: self.env,
}
}
}
impl<S: Isatty + WriteAll, T: Input> Input for EofGuard<'_, '_, S, T> {
#[allow(
clippy::await_holding_refcell_ref,
reason = "other decorators, the parser, or the executor do not run concurrently with this method"
)]
async fn next_line(&mut self, context: &Context) -> Result {
let mut remaining_tries = 50;
loop {
let line = self.inner.next_line(context).await?;
let env = self.env.borrow();
if !line.is_empty()
|| env.options.get(Interactive) == Off
|| remaining_tries == 0
|| !env.system.isatty(self.fd)
{
return Ok(line);
}
if env.options.get(PosixlyCorrect) == Off
&& let Some(config) = env.any.get::<SuspendedJobsGuardConfig>()
&& env.jobs.iter().any(|(_, job)| job.state.is_stopped())
{
env.system.print_error(&config.message).await;
} else if env.options.get(IgnoreEofOption) == On
&& let Some(config) = env.any.get::<IgnoreEofConfig>()
{
env.system.print_error(&config.message).await;
} else {
return Ok(line);
}
remaining_tries -= 1;
}
}
}
#[cfg(test)]
mod tests {
use super::super::Memory;
use super::*;
use crate::job::{Job, Pid, ProcessState};
use crate::option::On;
use crate::system::r#virtual::{FdBody, FileBody, Inode, OpenFileDescription, VirtualSystem};
use crate::system::{Concurrent, Mode};
use crate::test_helper::assert_stderr;
use enumset::EnumSet;
use futures_util::FutureExt as _;
use std::rc::Rc;
fn set_stdin_to_tty(system: &mut VirtualSystem) {
system
.current_process_mut()
.set_fd(
Fd::STDIN,
FdBody {
open_file_description: Rc::new(RefCell::new(OpenFileDescription::new(
Rc::new(RefCell::new(Inode {
body: FileBody::Terminal { content: vec![] },
permissions: Mode::empty(),
})),
0,
true,
true,
false,
false,
))),
flags: EnumSet::empty(),
},
)
.unwrap();
}
fn set_stdin_to_regular_file(system: &mut VirtualSystem) {
system
.current_process_mut()
.set_fd(
Fd::STDIN,
FdBody {
open_file_description: Rc::new(RefCell::new(OpenFileDescription::new(
Rc::new(RefCell::new(Inode {
body: FileBody::Regular {
content: vec![],
is_native_executable: false,
},
permissions: Mode::empty(),
})),
0,
true,
true,
false,
false,
))),
flags: EnumSet::empty(),
},
)
.unwrap();
}
struct EofStub<T> {
inner: T,
count: usize,
}
impl<T: Input> Input for EofStub<T> {
async fn next_line(&mut self, context: &Context) -> Result {
if let Some(remaining) = self.count.checked_sub(1) {
self.count = remaining;
Ok("".to_string())
} else {
self.inner.next_line(context).await
}
}
}
#[test]
fn decorator_reads_from_inner_input() {
let mut system = VirtualSystem::new();
set_stdin_to_tty(&mut system);
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
env.options.set(Interactive, On);
env.options.set(IgnoreEofOption, On);
env.any.insert(Box::new(IgnoreEofConfig::default()));
env.any
.insert(Box::new(SuspendedJobsGuardConfig::default()));
let ref_env = RefCell::new(&mut env);
let mut decorator = EofGuard::new(Memory::new("echo foo\n"), Fd::STDIN, &ref_env);
let result = decorator
.next_line(&Context::default())
.now_or_never()
.unwrap();
assert_eq!(result.unwrap(), "echo foo\n");
}
#[test]
fn decorator_reads_input_again_on_eof_with_ignore_eof_option() {
let mut system = VirtualSystem::new();
set_stdin_to_tty(&mut system);
let state = system.state.clone();
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
env.options.set(Interactive, On);
env.options.set(IgnoreEofOption, On);
env.any.insert(Box::new(IgnoreEofConfig {
message: "EOF ignored\n".to_string(),
}));
let ref_env = RefCell::new(&mut env);
let mut decorator = EofGuard::new(
EofStub {
inner: Memory::new("echo foo\n"),
count: 1,
},
Fd::STDIN,
&ref_env,
);
let result = decorator
.next_line(&Context::default())
.now_or_never()
.unwrap();
assert_eq!(result.unwrap(), "echo foo\n");
assert_stderr(&state, |stderr| assert_eq!(stderr, "EOF ignored\n"));
}
#[test]
fn decorator_reads_input_up_to_50_times() {
let mut system = VirtualSystem::new();
set_stdin_to_tty(&mut system);
let state = system.state.clone();
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
env.options.set(Interactive, On);
env.options.set(IgnoreEofOption, On);
env.any.insert(Box::new(IgnoreEofConfig {
message: "EOF ignored\n".to_string(),
}));
let ref_env = RefCell::new(&mut env);
let mut decorator = EofGuard::new(
EofStub {
inner: Memory::new("echo foo\n"),
count: 50,
},
Fd::STDIN,
&ref_env,
);
let result = decorator
.next_line(&Context::default())
.now_or_never()
.unwrap();
assert_eq!(result.unwrap(), "echo foo\n");
assert_stderr(&state, |stderr| {
assert_eq!(stderr, "EOF ignored\n".repeat(50))
});
}
#[test]
fn decorator_returns_empty_line_after_reading_51_times() {
let mut system = VirtualSystem::new();
set_stdin_to_tty(&mut system);
let state = system.state.clone();
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
env.options.set(Interactive, On);
env.options.set(IgnoreEofOption, On);
env.any.insert(Box::new(IgnoreEofConfig {
message: "EOF ignored\n".to_string(),
}));
let ref_env = RefCell::new(&mut env);
let mut decorator = EofGuard::new(
EofStub {
inner: Memory::new("echo foo\n"),
count: 51,
},
Fd::STDIN,
&ref_env,
);
let result = decorator
.next_line(&Context::default())
.now_or_never()
.unwrap();
assert_eq!(result.unwrap(), "");
assert_stderr(&state, |stderr| {
assert_eq!(stderr, "EOF ignored\n".repeat(50))
});
}
#[test]
fn decorator_returns_immediately_if_not_interactive() {
let mut system = VirtualSystem::new();
set_stdin_to_tty(&mut system);
let state = system.state.clone();
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
env.options.set(IgnoreEofOption, On);
env.any.insert(Box::new(IgnoreEofConfig::default()));
env.any
.insert(Box::new(SuspendedJobsGuardConfig::default()));
let ref_env = RefCell::new(&mut env);
let mut decorator = EofGuard::new(
EofStub {
inner: Memory::new("echo foo\n"),
count: 1,
},
Fd::STDIN,
&ref_env,
);
let result = decorator
.next_line(&Context::default())
.now_or_never()
.unwrap();
assert_eq!(result.unwrap(), "");
assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
}
#[test]
fn decorator_returns_immediately_if_not_ignore_eof_and_no_suspended_jobs() {
let mut system = VirtualSystem::new();
set_stdin_to_tty(&mut system);
let state = system.state.clone();
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
env.options.set(Interactive, On);
env.any.insert(Box::new(IgnoreEofConfig::default()));
env.any
.insert(Box::new(SuspendedJobsGuardConfig::default()));
let ref_env = RefCell::new(&mut env);
let mut decorator = EofGuard::new(
EofStub {
inner: Memory::new("echo foo\n"),
count: 1,
},
Fd::STDIN,
&ref_env,
);
let result = decorator
.next_line(&Context::default())
.now_or_never()
.unwrap();
assert_eq!(result.unwrap(), "");
assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
}
#[test]
fn decorator_returns_immediately_if_not_terminal() {
let mut system = VirtualSystem::new();
set_stdin_to_regular_file(&mut system);
let state = system.state.clone();
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
env.options.set(Interactive, On);
env.options.set(IgnoreEofOption, On);
env.any.insert(Box::new(IgnoreEofConfig::default()));
env.any
.insert(Box::new(SuspendedJobsGuardConfig::default()));
let ref_env = RefCell::new(&mut env);
let mut decorator = EofGuard::new(
EofStub {
inner: Memory::new("echo foo\n"),
count: 1,
},
Fd::STDIN,
&ref_env,
);
let result = decorator
.next_line(&Context::default())
.now_or_never()
.unwrap();
assert_eq!(result.unwrap(), "");
assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
}
#[test]
fn decorator_returns_immediately_if_no_ignore_eof_config() {
let mut system = VirtualSystem::new();
set_stdin_to_tty(&mut system);
let state = system.state.clone();
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
env.options.set(Interactive, On);
env.options.set(IgnoreEofOption, On);
let ref_env = RefCell::new(&mut env);
let mut decorator = EofGuard::new(
EofStub {
inner: Memory::new("echo foo\n"),
count: 1,
},
Fd::STDIN,
&ref_env,
);
let result = decorator
.next_line(&Context::default())
.now_or_never()
.unwrap();
assert_eq!(result.unwrap(), "");
assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
}
#[test]
fn decorator_ignores_eof_when_there_are_suspended_jobs() {
let mut system = VirtualSystem::new();
set_stdin_to_tty(&mut system);
let state = system.state.clone();
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
env.options.set(Interactive, On);
let mut job = Job::new(Pid(42));
job.state = ProcessState::stopped(crate::system::r#virtual::SIGTSTP);
env.jobs.insert(job);
env.any.insert(Box::new(SuspendedJobsGuardConfig {
message: "There are stopped jobs.\n".to_string(),
}));
let ref_env = RefCell::new(&mut env);
let mut decorator = EofGuard::new(
EofStub {
inner: Memory::new("echo foo\n"),
count: 1,
},
Fd::STDIN,
&ref_env,
);
let result = decorator
.next_line(&Context::default())
.now_or_never()
.unwrap();
assert_eq!(result.unwrap(), "echo foo\n");
assert_stderr(&state, |stderr| {
assert_eq!(stderr, "There are stopped jobs.\n")
});
}
#[test]
fn decorator_returns_immediately_if_posixly_correct_with_suspended_jobs() {
let mut system = VirtualSystem::new();
set_stdin_to_tty(&mut system);
let state = system.state.clone();
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
env.options.set(Interactive, On);
env.options.set(PosixlyCorrect, On);
let mut job = Job::new(Pid(42));
job.state = ProcessState::stopped(crate::system::r#virtual::SIGTSTP);
env.jobs.insert(job);
env.any.insert(Box::new(SuspendedJobsGuardConfig {
message: "There are stopped jobs.\n".to_string(),
}));
let ref_env = RefCell::new(&mut env);
let mut decorator = EofGuard::new(
EofStub {
inner: Memory::new("echo foo\n"),
count: 1,
},
Fd::STDIN,
&ref_env,
);
let result = decorator
.next_line(&Context::default())
.now_or_never()
.unwrap();
assert_eq!(result.unwrap(), "");
assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
}
#[test]
fn decorator_returns_immediately_if_no_suspended_jobs_config() {
let mut system = VirtualSystem::new();
set_stdin_to_tty(&mut system);
let state = system.state.clone();
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
env.options.set(Interactive, On);
let mut job = Job::new(Pid(42));
job.state = ProcessState::stopped(crate::system::r#virtual::SIGTSTP);
env.jobs.insert(job);
let ref_env = RefCell::new(&mut env);
let mut decorator = EofGuard::new(
EofStub {
inner: Memory::new("echo foo\n"),
count: 1,
},
Fd::STDIN,
&ref_env,
);
let result = decorator
.next_line(&Context::default())
.now_or_never()
.unwrap();
assert_eq!(result.unwrap(), "");
assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
}
#[test]
fn decorator_falls_back_to_ignore_eof_when_no_suspended_jobs_config() {
let mut system = VirtualSystem::new();
set_stdin_to_tty(&mut system);
let state = system.state.clone();
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
env.options.set(Interactive, On);
env.options.set(IgnoreEofOption, On);
let mut job = Job::new(Pid(42));
job.state = ProcessState::stopped(crate::system::r#virtual::SIGTSTP);
env.jobs.insert(job);
env.any.insert(Box::new(IgnoreEofConfig {
message: "EOF ignored\n".to_string(),
}));
let ref_env = RefCell::new(&mut env);
let mut decorator = EofGuard::new(
EofStub {
inner: Memory::new("echo foo\n"),
count: 1,
},
Fd::STDIN,
&ref_env,
);
let result = decorator
.next_line(&Context::default())
.now_or_never()
.unwrap();
assert_eq!(result.unwrap(), "echo foo\n");
assert_stderr(&state, |stderr| assert_eq!(stderr, "EOF ignored\n"));
}
#[test]
fn suspended_jobs_message_takes_priority_over_ignore_eof_message() {
let mut system = VirtualSystem::new();
set_stdin_to_tty(&mut system);
let state = system.state.clone();
let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
env.options.set(Interactive, On);
env.options.set(IgnoreEofOption, On);
let mut job = Job::new(Pid(42));
job.state = ProcessState::stopped(crate::system::r#virtual::SIGTSTP);
env.jobs.insert(job);
env.any.insert(Box::new(IgnoreEofConfig {
message: "EOF ignored\n".to_string(),
}));
env.any.insert(Box::new(SuspendedJobsGuardConfig {
message: "There are stopped jobs.\n".to_string(),
}));
let ref_env = RefCell::new(&mut env);
let mut decorator = EofGuard::new(
EofStub {
inner: Memory::new("echo foo\n"),
count: 1,
},
Fd::STDIN,
&ref_env,
);
let result = decorator
.next_line(&Context::default())
.now_or_never()
.unwrap();
assert_eq!(result.unwrap(), "echo foo\n");
assert_stderr(&state, |stderr| {
assert_eq!(stderr, "There are stopped jobs.\n")
});
}
}