Skip to main content

yash_env/input/
reporter.rs

1// This file is part of yash, an extended POSIX shell.
2// Copyright (C) 2024 WATANABE Yuki
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17use super::{Context, Input, Result};
18use crate::Env;
19use crate::io::Fd;
20use crate::job::fmt::Accumulator;
21use crate::option::{Interactive, Monitor, Off};
22use crate::system::Signals;
23use crate::system::concurrency::WriteAll;
24use std::cell::RefCell;
25
26/// `Input` decorator that reports job status changes before reading a line
27///
28/// This decorator prints the status of jobs that have changed since the last
29/// report. The status is printed to the standard error before the input is read.
30/// This is done only if the [`Interactive`] and [`Monitor`] options are enabled.
31#[derive(Debug)]
32pub struct Reporter<'a, 'b, S, T> {
33    inner: T,
34    env: &'a RefCell<&'b mut Env<S>>,
35}
36
37impl<'a, 'b, S, T> Reporter<'a, 'b, S, T> {
38    /// Creates a new `Reporter` decorator.
39    ///
40    /// The first argument is the inner `Input` that performs the actual input
41    /// operation. The second argument is the shell environment that contains
42    /// the shell option state and the system interface to print to the standard
43    /// error. It is wrapped in a `RefCell` so that it can be shared with other
44    /// decorators and the parser.
45    pub fn new(inner: T, env: &'a RefCell<&'b mut Env<S>>) -> Self {
46        Self { inner, env }
47    }
48}
49
50// Not derived automatically because S may not implement Clone.
51impl<S, T: Clone> Clone for Reporter<'_, '_, S, T> {
52    fn clone(&self) -> Self {
53        Self {
54            inner: self.inner.clone(),
55            env: self.env,
56        }
57    }
58}
59
60impl<S: Signals + WriteAll, T: Input> Input for Reporter<'_, '_, S, T> {
61    #[allow(
62        clippy::await_holding_refcell_ref,
63        reason = "other decorators, the parser, or the executor do not run concurrently with this method"
64    )]
65    async fn next_line(&mut self, context: &Context) -> Result {
66        report(&mut self.env.borrow_mut()).await;
67        self.inner.next_line(context).await
68    }
69}
70
71async fn report<S: Signals + WriteAll>(env: &mut Env<S>) {
72    if env.options.get(Interactive) == Off || env.options.get(Monitor) == Off {
73        return;
74    }
75
76    let mut accumulator = Accumulator::new();
77    accumulator.current_job_index = env.jobs.current_job();
78    accumulator.previous_job_index = env.jobs.previous_job();
79    env.jobs
80        .iter()
81        .filter(|(_, job)| job.state_changed)
82        .for_each(|(index, job)| accumulator.add(index, job, &env.system));
83
84    if env
85        .system
86        .write_all(Fd::STDERR, accumulator.print.as_bytes())
87        .await
88        .is_ok()
89    {
90        for index in accumulator.indices_reported {
91            env.jobs.get_mut(index).unwrap().state_reported();
92        }
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::super::Memory;
99    use super::*;
100    use crate::VirtualSystem;
101    use crate::job::{Job, Pid, ProcessState};
102    use crate::option::On;
103    use crate::system::Concurrent;
104    use crate::system::r#virtual::SystemState;
105    use crate::test_helper::assert_stderr;
106    use futures_util::FutureExt as _;
107    use std::rc::Rc;
108
109    #[test]
110    fn reporter_reads_from_inner_input() {
111        let mut env = Env::new_virtual();
112        let ref_env = RefCell::new(&mut env);
113        let mut reporter = Reporter::new(Memory::new("echo hello"), &ref_env);
114        let result = reporter
115            .next_line(&Context::default())
116            .now_or_never()
117            .unwrap();
118        assert_eq!(result.unwrap(), "echo hello");
119    }
120
121    #[test]
122    fn reporter_shows_job_status_before_reading_input() {
123        let system = VirtualSystem::new();
124        let state = system.state.clone();
125        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
126        env.jobs.add({
127            let mut job = Job::new(Pid(10));
128            job.state_changed = true;
129            job.name = "echo hello".to_string();
130            job
131        });
132        env.options.set(Interactive, On);
133        env.options.set(Monitor, On);
134
135        struct InputMock(Rc<RefCell<SystemState>>);
136        impl Input for InputMock {
137            async fn next_line(&mut self, _: &Context) -> Result {
138                // The Report is expected to have shown the report before
139                // calling the inner input. Let's check that here.
140                assert_stderr(&self.0, |stderr| {
141                    assert!(stderr.starts_with("[1]"), "stderr: {stderr:?}")
142                });
143
144                Ok("foo".to_string())
145            }
146        }
147
148        let ref_env = RefCell::new(&mut env);
149        let mut reporter = Reporter::new(InputMock(state), &ref_env);
150        let result = reporter
151            .next_line(&Context::default())
152            .now_or_never()
153            .unwrap();
154        assert_eq!(result.unwrap(), "foo"); // Make sure the mock input is called.
155    }
156
157    #[test]
158    fn all_jobs_with_changed_status_are_reported() {
159        let system = VirtualSystem::new();
160        let state = system.state.clone();
161        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
162        env.jobs.add({
163            let mut job = Job::new(Pid(10));
164            job.state_changed = true;
165            job.name = "echo hello".to_string();
166            job
167        });
168        env.jobs.add({
169            let mut job = Job::new(Pid(20));
170            job.state_changed = false;
171            job.name = "sleep 1".to_string();
172            job
173        });
174        env.jobs.add({
175            let mut job = Job::new(Pid(30));
176            job.state = ProcessState::exited(0);
177            job.state_changed = true;
178            job.name = "cat README".to_string();
179            job
180        });
181        env.options.set(Interactive, On);
182        env.options.set(Monitor, On);
183        let ref_env = RefCell::new(&mut env);
184        let memory = Memory::new("echo hello\n");
185        let mut reporter = Reporter::new(memory, &ref_env);
186
187        reporter
188            .next_line(&Context::default())
189            .now_or_never()
190            .unwrap()
191            .unwrap();
192        assert_stderr(&state, |stderr| {
193            let mut lines = stderr.lines();
194            let first = lines.next().unwrap();
195            assert!(first.starts_with("[1]"), "first: {first:?}");
196            assert!(first.contains("Running"), "first: {first:?}");
197            assert!(first.contains("echo hello"), "first: {first:?}");
198            let second = lines.next().unwrap();
199            assert!(second.starts_with("[3]"), "second: {second:?}");
200            assert!(second.contains("Done"), "second: {second:?}");
201            assert!(second.contains("cat README"), "second: {second:?}");
202            assert_eq!(lines.next(), None);
203        });
204    }
205
206    #[test]
207    fn reporter_clears_state_changed_flag() {
208        let mut env = Env::new_virtual();
209        let index = env.jobs.add({
210            let mut job = Job::new(Pid(10));
211            job.state_changed = true;
212            job.name = "echo hello".to_string();
213            job
214        });
215        env.options.set(Interactive, On);
216        env.options.set(Monitor, On);
217        let ref_env = RefCell::new(&mut env);
218        let memory = Memory::new("echo hello\n");
219        let mut reporter = Reporter::new(memory, &ref_env);
220
221        reporter
222            .next_line(&Context::default())
223            .now_or_never()
224            .unwrap()
225            .unwrap();
226        assert!(!env.jobs.get(index).unwrap().state_changed);
227    }
228
229    #[test]
230    fn no_report_if_not_interactive() {
231        let system = VirtualSystem::new();
232        let state = system.state.clone();
233        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
234        env.jobs.add({
235            let mut job = Job::new(Pid(10));
236            job.state_changed = true;
237            job.name = "echo hello".to_string();
238            job
239        });
240        env.options.set(Monitor, On);
241        let ref_env = RefCell::new(&mut env);
242        let memory = Memory::new("echo hello\n");
243        let mut reporter = Reporter::new(memory, &ref_env);
244
245        reporter
246            .next_line(&Context::default())
247            .now_or_never()
248            .unwrap()
249            .unwrap();
250        assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
251    }
252
253    #[test]
254    fn no_report_if_not_monitor() {
255        let system = VirtualSystem::new();
256        let state = system.state.clone();
257        let mut env = Env::with_system(Rc::new(Concurrent::new(system)));
258        env.jobs.add({
259            let mut job = Job::new(Pid(10));
260            job.state_changed = true;
261            job.name = "echo hello".to_string();
262            job
263        });
264        env.options.set(Interactive, On);
265        let ref_env = RefCell::new(&mut env);
266        let memory = Memory::new("echo hello\n");
267        let mut reporter = Reporter::new(memory, &ref_env);
268
269        reporter
270            .next_line(&Context::default())
271            .now_or_never()
272            .unwrap()
273            .unwrap();
274        assert_stderr(&state, |stderr| assert_eq!(stderr, ""));
275    }
276}