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