1use super::{Context, Input, Result};
21use crate::Env;
22use crate::io::Fd;
23use crate::option::{IgnoreEof as IgnoreEofOption, Interactive, Off, On, PosixlyCorrect};
24use crate::system::Isatty;
25use crate::system::concurrency::WriteAll;
26use std::cell::RefCell;
27
28#[derive(Clone, Debug, Default)]
38#[non_exhaustive]
39pub struct SuspendedJobsGuardConfig {
40 pub message: String,
42}
43
44impl SuspendedJobsGuardConfig {
45 #[must_use]
47 pub fn with_message<M: Into<String>>(message: M) -> Self {
48 Self {
49 message: message.into(),
50 }
51 }
52}
53
54#[derive(Clone, Debug, Default)]
68#[non_exhaustive]
69pub struct IgnoreEofConfig {
70 pub message: String,
72}
73
74impl IgnoreEofConfig {
75 #[must_use]
77 pub fn with_message<M: Into<String>>(message: M) -> Self {
78 Self {
79 message: message.into(),
80 }
81 }
82}
83
84#[derive(Debug)]
111pub struct EofGuard<'a, 'b, S, T> {
112 inner: T,
114 fd: Fd,
116 env: &'a RefCell<&'b mut Env<S>>,
118}
119
120impl<'a, 'b, S, T> EofGuard<'a, 'b, S, T> {
121 pub fn new(inner: T, fd: Fd, env: &'a RefCell<&'b mut Env<S>>) -> Self {
130 Self { inner, fd, env }
131 }
132}
133
134impl<S, T: Clone> Clone for EofGuard<'_, '_, S, T> {
136 fn clone(&self) -> Self {
137 Self {
138 inner: self.inner.clone(),
139 fd: self.fd,
140 env: self.env,
141 }
142 }
143}
144
145impl<S: Isatty + WriteAll, T: Input> Input for EofGuard<'_, '_, S, T> {
146 #[allow(
147 clippy::await_holding_refcell_ref,
148 reason = "other decorators, the parser, or the executor do not run concurrently with this method"
149 )]
150 async fn next_line(&mut self, context: &Context) -> Result {
151 let mut remaining_tries = 50;
152
153 loop {
154 let line = self.inner.next_line(context).await?;
155
156 let env = self.env.borrow();
157
158 if !line.is_empty()
159 || env.options.get(Interactive) == Off
160 || remaining_tries == 0
161 || !env.system.isatty(self.fd)
162 {
163 return Ok(line);
164 }
165
166 if env.options.get(PosixlyCorrect) == Off
167 && let Some(config) = env.any.get::<SuspendedJobsGuardConfig>()
168 && env.jobs.iter().any(|(_, job)| job.state.is_stopped())
169 {
170 env.system.print_error(&config.message).await;
171 } else if env.options.get(IgnoreEofOption) == On
172 && let Some(config) = env.any.get::<IgnoreEofConfig>()
173 {
174 env.system.print_error(&config.message).await;
175 } else {
176 return Ok(line);
177 }
178
179 remaining_tries -= 1;
180 }
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::super::Memory;
187 use super::*;
188 use crate::job::{Job, Pid, ProcessState};
189 use crate::option::On;
190 use crate::system::r#virtual::{FdBody, FileBody, Inode, OpenFileDescription, VirtualSystem};
191 use crate::system::{Concurrent, Mode};
192 use crate::test_helper::assert_stderr;
193 use enumset::EnumSet;
194 use futures_util::FutureExt as _;
195 use std::rc::Rc;
196
197 fn set_stdin_to_tty(system: &mut VirtualSystem) {
198 system
199 .current_process_mut()
200 .set_fd(
201 Fd::STDIN,
202 FdBody {
203 open_file_description: Rc::new(RefCell::new(OpenFileDescription::new(
204 Rc::new(RefCell::new(Inode {
205 body: FileBody::Terminal { content: vec![] },
206 permissions: Mode::empty(),
207 })),
208 0,
209 true,
210 true,
211 false,
212 false,
213 ))),
214 flags: EnumSet::empty(),
215 },
216 )
217 .unwrap();
218 }
219
220 fn set_stdin_to_regular_file(system: &mut VirtualSystem) {
221 system
222 .current_process_mut()
223 .set_fd(
224 Fd::STDIN,
225 FdBody {
226 open_file_description: Rc::new(RefCell::new(OpenFileDescription::new(
227 Rc::new(RefCell::new(Inode {
228 body: FileBody::Regular {
229 content: vec![],
230 is_native_executable: false,
231 },
232 permissions: Mode::empty(),
233 })),
234 0,
235 true,
236 true,
237 false,
238 false,
239 ))),
240 flags: EnumSet::empty(),
241 },
242 )
243 .unwrap();
244 }
245
246 struct EofStub<T> {
249 inner: T,
250 count: usize,
251 }
252
253 impl<T: Input> Input for EofStub<T> {
254 async fn next_line(&mut self, context: &Context) -> Result {
255 if let Some(remaining) = self.count.checked_sub(1) {
256 self.count = remaining;
257 Ok("".to_string())
258 } else {
259 self.inner.next_line(context).await
260 }
261 }
262 }
263
264 #[test]
265 fn decorator_reads_from_inner_input() {
266 let mut system = VirtualSystem::new();
267 set_stdin_to_tty(&mut system);
268 let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
269 env.options.set(Interactive, On);
270 env.options.set(IgnoreEofOption, On);
271 env.any.insert(Box::new(IgnoreEofConfig::default()));
272 env.any
273 .insert(Box::new(SuspendedJobsGuardConfig::default()));
274 let ref_env = RefCell::new(&mut env);
275 let mut decorator = EofGuard::new(Memory::new("echo foo\n"), Fd::STDIN, &ref_env);
276
277 let result = decorator
278 .next_line(&Context::default())
279 .now_or_never()
280 .unwrap();
281 assert_eq!(result.unwrap(), "echo foo\n");
282 }
283
284 #[test]
285 fn decorator_reads_input_again_on_eof_with_ignore_eof_option() {
286 let mut system = VirtualSystem::new();
287 set_stdin_to_tty(&mut system);
288 let state = system.state.clone();
289 let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
290 env.options.set(Interactive, On);
291 env.options.set(IgnoreEofOption, On);
292 env.any.insert(Box::new(IgnoreEofConfig {
293 message: "EOF ignored\n".to_string(),
294 }));
295 let ref_env = RefCell::new(&mut env);
296 let mut decorator = EofGuard::new(
297 EofStub {
298 inner: Memory::new("echo foo\n"),
299 count: 1,
300 },
301 Fd::STDIN,
302 &ref_env,
303 );
304
305 let result = decorator
306 .next_line(&Context::default())
307 .now_or_never()
308 .unwrap();
309 assert_eq!(result.unwrap(), "echo foo\n");
310 assert_stderr(&state, |stderr| assert_eq!(stderr, "EOF ignored\n"));
311 }
312
313 #[test]
314 fn decorator_reads_input_up_to_50_times() {
315 let mut system = VirtualSystem::new();
316 set_stdin_to_tty(&mut system);
317 let state = system.state.clone();
318 let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
319 env.options.set(Interactive, On);
320 env.options.set(IgnoreEofOption, On);
321 env.any.insert(Box::new(IgnoreEofConfig {
322 message: "EOF ignored\n".to_string(),
323 }));
324 let ref_env = RefCell::new(&mut env);
325 let mut decorator = EofGuard::new(
326 EofStub {
327 inner: Memory::new("echo foo\n"),
328 count: 50,
329 },
330 Fd::STDIN,
331 &ref_env,
332 );
333
334 let result = decorator
335 .next_line(&Context::default())
336 .now_or_never()
337 .unwrap();
338 assert_eq!(result.unwrap(), "echo foo\n");
339 assert_stderr(&state, |stderr| {
340 assert_eq!(stderr, "EOF ignored\n".repeat(50))
341 });
342 }
343
344 #[test]
345 fn decorator_returns_empty_line_after_reading_51_times() {
346 let mut system = VirtualSystem::new();
347 set_stdin_to_tty(&mut system);
348 let state = system.state.clone();
349 let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
350 env.options.set(Interactive, On);
351 env.options.set(IgnoreEofOption, On);
352 env.any.insert(Box::new(IgnoreEofConfig {
353 message: "EOF ignored\n".to_string(),
354 }));
355 let ref_env = RefCell::new(&mut env);
356 let mut decorator = EofGuard::new(
357 EofStub {
358 inner: Memory::new("echo foo\n"),
359 count: 51,
360 },
361 Fd::STDIN,
362 &ref_env,
363 );
364
365 let result = decorator
366 .next_line(&Context::default())
367 .now_or_never()
368 .unwrap();
369 assert_eq!(result.unwrap(), "");
370 assert_stderr(&state, |stderr| {
371 assert_eq!(stderr, "EOF ignored\n".repeat(50))
372 });
373 }
374
375 #[test]
376 fn decorator_returns_immediately_if_not_interactive() {
377 let mut system = VirtualSystem::new();
378 set_stdin_to_tty(&mut system);
379 let state = system.state.clone();
380 let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
381 env.options.set(IgnoreEofOption, On);
383 env.any.insert(Box::new(IgnoreEofConfig::default()));
384 env.any
385 .insert(Box::new(SuspendedJobsGuardConfig::default()));
386 let ref_env = RefCell::new(&mut env);
387 let mut decorator = EofGuard::new(
388 EofStub {
389 inner: Memory::new("echo foo\n"),
390 count: 1,
391 },
392 Fd::STDIN,
393 &ref_env,
394 );
395
396 let result = decorator
397 .next_line(&Context::default())
398 .now_or_never()
399 .unwrap();
400 assert_eq!(result.unwrap(), "");
401 assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
402 }
403
404 #[test]
405 fn decorator_returns_immediately_if_not_ignore_eof_and_no_suspended_jobs() {
406 let mut system = VirtualSystem::new();
407 set_stdin_to_tty(&mut system);
408 let state = system.state.clone();
409 let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
410 env.options.set(Interactive, On);
411 env.any.insert(Box::new(IgnoreEofConfig::default()));
413 env.any
414 .insert(Box::new(SuspendedJobsGuardConfig::default()));
415 let ref_env = RefCell::new(&mut env);
416 let mut decorator = EofGuard::new(
417 EofStub {
418 inner: Memory::new("echo foo\n"),
419 count: 1,
420 },
421 Fd::STDIN,
422 &ref_env,
423 );
424
425 let result = decorator
426 .next_line(&Context::default())
427 .now_or_never()
428 .unwrap();
429 assert_eq!(result.unwrap(), "");
430 assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
431 }
432
433 #[test]
434 fn decorator_returns_immediately_if_not_terminal() {
435 let mut system = VirtualSystem::new();
436 set_stdin_to_regular_file(&mut system);
437 let state = system.state.clone();
438 let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
439 env.options.set(Interactive, On);
440 env.options.set(IgnoreEofOption, On);
441 env.any.insert(Box::new(IgnoreEofConfig::default()));
442 env.any
443 .insert(Box::new(SuspendedJobsGuardConfig::default()));
444 let ref_env = RefCell::new(&mut env);
445 let mut decorator = EofGuard::new(
446 EofStub {
447 inner: Memory::new("echo foo\n"),
448 count: 1,
449 },
450 Fd::STDIN,
451 &ref_env,
452 );
453
454 let result = decorator
455 .next_line(&Context::default())
456 .now_or_never()
457 .unwrap();
458 assert_eq!(result.unwrap(), "");
459 assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
460 }
461
462 #[test]
466 fn decorator_returns_immediately_if_no_ignore_eof_config() {
467 let mut system = VirtualSystem::new();
468 set_stdin_to_tty(&mut system);
469 let state = system.state.clone();
470 let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
471 env.options.set(Interactive, On);
472 env.options.set(IgnoreEofOption, On);
473 let ref_env = RefCell::new(&mut env);
475 let mut decorator = EofGuard::new(
476 EofStub {
477 inner: Memory::new("echo foo\n"),
478 count: 1,
479 },
480 Fd::STDIN,
481 &ref_env,
482 );
483
484 let result = decorator
485 .next_line(&Context::default())
486 .now_or_never()
487 .unwrap();
488 assert_eq!(result.unwrap(), "");
489 assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
490 }
491
492 #[test]
493 fn decorator_ignores_eof_when_there_are_suspended_jobs() {
494 let mut system = VirtualSystem::new();
495 set_stdin_to_tty(&mut system);
496 let state = system.state.clone();
497 let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
498 env.options.set(Interactive, On);
499 let mut job = Job::new(Pid(42));
501 job.state = ProcessState::stopped(crate::system::r#virtual::SIGTSTP);
502 env.jobs.insert(job);
503 env.any.insert(Box::new(SuspendedJobsGuardConfig {
504 message: "There are stopped jobs.\n".to_string(),
505 }));
506 let ref_env = RefCell::new(&mut env);
507 let mut decorator = EofGuard::new(
508 EofStub {
509 inner: Memory::new("echo foo\n"),
510 count: 1,
511 },
512 Fd::STDIN,
513 &ref_env,
514 );
515
516 let result = decorator
517 .next_line(&Context::default())
518 .now_or_never()
519 .unwrap();
520 assert_eq!(result.unwrap(), "echo foo\n");
521 assert_stderr(&state, |stderr| {
522 assert_eq!(stderr, "There are stopped jobs.\n")
523 });
524 }
525
526 #[test]
527 fn decorator_returns_immediately_if_posixly_correct_with_suspended_jobs() {
528 let mut system = VirtualSystem::new();
529 set_stdin_to_tty(&mut system);
530 let state = system.state.clone();
531 let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
532 env.options.set(Interactive, On);
533 env.options.set(PosixlyCorrect, On);
534 let mut job = Job::new(Pid(42));
535 job.state = ProcessState::stopped(crate::system::r#virtual::SIGTSTP);
536 env.jobs.insert(job);
537 env.any.insert(Box::new(SuspendedJobsGuardConfig {
538 message: "There are stopped jobs.\n".to_string(),
539 }));
540 let ref_env = RefCell::new(&mut env);
541 let mut decorator = EofGuard::new(
542 EofStub {
543 inner: Memory::new("echo foo\n"),
544 count: 1,
545 },
546 Fd::STDIN,
547 &ref_env,
548 );
549
550 let result = decorator
551 .next_line(&Context::default())
552 .now_or_never()
553 .unwrap();
554 assert_eq!(result.unwrap(), "");
555 assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
556 }
557
558 #[test]
562 fn decorator_returns_immediately_if_no_suspended_jobs_config() {
563 let mut system = VirtualSystem::new();
564 set_stdin_to_tty(&mut system);
565 let state = system.state.clone();
566 let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
567 env.options.set(Interactive, On);
568 let mut job = Job::new(Pid(42));
570 job.state = ProcessState::stopped(crate::system::r#virtual::SIGTSTP);
571 env.jobs.insert(job);
572 let ref_env = RefCell::new(&mut env);
574 let mut decorator = EofGuard::new(
575 EofStub {
576 inner: Memory::new("echo foo\n"),
577 count: 1,
578 },
579 Fd::STDIN,
580 &ref_env,
581 );
582
583 let result = decorator
584 .next_line(&Context::default())
585 .now_or_never()
586 .unwrap();
587 assert_eq!(result.unwrap(), "");
588 assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
589 }
590
591 #[test]
595 fn decorator_falls_back_to_ignore_eof_when_no_suspended_jobs_config() {
596 let mut system = VirtualSystem::new();
597 set_stdin_to_tty(&mut system);
598 let state = system.state.clone();
599 let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
600 env.options.set(Interactive, On);
601 env.options.set(IgnoreEofOption, On);
602 let mut job = Job::new(Pid(42));
603 job.state = ProcessState::stopped(crate::system::r#virtual::SIGTSTP);
604 env.jobs.insert(job);
605 env.any.insert(Box::new(IgnoreEofConfig {
607 message: "EOF ignored\n".to_string(),
608 }));
609 let ref_env = RefCell::new(&mut env);
610 let mut decorator = EofGuard::new(
611 EofStub {
612 inner: Memory::new("echo foo\n"),
613 count: 1,
614 },
615 Fd::STDIN,
616 &ref_env,
617 );
618
619 let result = decorator
620 .next_line(&Context::default())
621 .now_or_never()
622 .unwrap();
623 assert_eq!(result.unwrap(), "echo foo\n");
624 assert_stderr(&state, |stderr| assert_eq!(stderr, "EOF ignored\n"));
625 }
626
627 #[test]
628 fn suspended_jobs_message_takes_priority_over_ignore_eof_message() {
629 let mut system = VirtualSystem::new();
634 set_stdin_to_tty(&mut system);
635 let state = system.state.clone();
636 let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
637 env.options.set(Interactive, On);
638 env.options.set(IgnoreEofOption, On);
639 let mut job = Job::new(Pid(42));
640 job.state = ProcessState::stopped(crate::system::r#virtual::SIGTSTP);
641 env.jobs.insert(job);
642 env.any.insert(Box::new(IgnoreEofConfig {
643 message: "EOF ignored\n".to_string(),
644 }));
645 env.any.insert(Box::new(SuspendedJobsGuardConfig {
646 message: "There are stopped jobs.\n".to_string(),
647 }));
648 let ref_env = RefCell::new(&mut env);
649 let mut decorator = EofGuard::new(
650 EofStub {
651 inner: Memory::new("echo foo\n"),
652 count: 1,
653 },
654 Fd::STDIN,
655 &ref_env,
656 );
657
658 let result = decorator
659 .next_line(&Context::default())
660 .now_or_never()
661 .unwrap();
662 assert_eq!(result.unwrap(), "echo foo\n");
663 assert_stderr(&state, |stderr| {
664 assert_eq!(stderr, "There are stopped jobs.\n")
665 });
666 }
667}