Skip to main content

standout_dispatch/
stream.rs

1//! The one destination a run's rendered bytes reach.
2//!
3//! Every byte a run produces goes through a [`StreamSink`]: a handler's
4//! incremental events as they are emitted, then the result or the diagnostic,
5//! then the warning entries. The process edge writes through it to stdout; a
6//! capture entry point hands it a [`StreamCapture`] and reads the bytes back;
7//! an output file override retargets it before the handler runs, so the file
8//! receives the whole run and stdout nothing — [`StreamSink::redirect`] straight
9//! away, or [`StreamSink::redirect_on_first_write`] when a run that writes
10//! nothing should leave the file uncreated.
11//!
12//! The sink classifies a `BrokenPipe` from its writer rather than reporting
13//! it: a reader that left closes the sink, every later write is discarded and
14//! reports success, and the handler runs to completion and the run reports the
15//! command's own status.
16
17use std::cell::RefCell;
18use std::fmt;
19use std::io::{ErrorKind, Write};
20use std::rc::Rc;
21
22type OpenWriter = Box<dyn FnOnce() -> std::io::Result<Box<dyn Write>>>;
23
24struct Destination {
25    writer: Box<dyn Write>,
26    pending: Option<OpenWriter>,
27    /// A deferred destination that could not be opened. The redirect replaced
28    /// the writer the run would otherwise have used, so every later write
29    /// reports the same reason rather than falling back to that writer.
30    unopened: Option<(ErrorKind, String)>,
31    open: bool,
32}
33
34impl Destination {
35    fn ready(&mut self) -> std::io::Result<()> {
36        if let Some((kind, message)) = &self.unopened {
37            return Err(std::io::Error::new(*kind, message.clone()));
38        }
39        let Some(open) = self.pending.take() else {
40            return Ok(());
41        };
42        match open() {
43            Ok(writer) => {
44                self.writer = writer;
45                Ok(())
46            }
47            Err(error) => {
48                self.unopened = Some((error.kind(), error.to_string()));
49                Err(error)
50            }
51        }
52    }
53}
54
55impl Write for Destination {
56    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
57        if !self.open {
58            return Ok(buf.len());
59        }
60        self.ready()?;
61        match self.writer.write(buf) {
62            Err(error) if error.kind() == ErrorKind::BrokenPipe => {
63                self.open = false;
64                Ok(buf.len())
65            }
66            other => other,
67        }
68    }
69
70    fn flush(&mut self) -> std::io::Result<()> {
71        if !self.open {
72            return Ok(());
73        }
74        if let Some((kind, message)) = &self.unopened {
75            return Err(std::io::Error::new(*kind, message.clone()));
76        }
77        if self.pending.is_some() {
78            return Ok(());
79        }
80        match self.writer.flush() {
81            Err(error) if error.kind() == ErrorKind::BrokenPipe => {
82                self.open = false;
83                Ok(())
84            }
85            other => other,
86        }
87    }
88}
89
90#[derive(Clone)]
91pub struct StreamSink(Rc<RefCell<Destination>>);
92
93impl StreamSink {
94    pub fn new(writer: impl Write + 'static) -> Self {
95        Self(Rc::new(RefCell::new(Destination {
96            writer: Box::new(writer),
97            pending: None,
98            unopened: None,
99            open: true,
100        })))
101    }
102
103    pub fn process_stdout() -> Self {
104        Self::new(std::io::stdout())
105    }
106
107    /// Replace the destination; every clone of this sink follows.
108    pub fn redirect(&self, writer: impl Write + 'static) {
109        let mut destination = self.0.borrow_mut();
110        destination.writer = Box::new(writer);
111        destination.pending = None;
112        destination.unopened = None;
113        destination.open = true;
114    }
115
116    /// Replace the destination when there is a first byte to write, so a run
117    /// that writes none leaves the writer unopened. Failing to open it fails
118    /// that write and every later one.
119    pub fn redirect_on_first_write<W: Write + 'static>(
120        &self,
121        open: impl FnOnce() -> std::io::Result<W> + 'static,
122    ) {
123        let mut destination = self.0.borrow_mut();
124        destination.pending = Some(Box::new(move || {
125            open().map(|w| Box::new(w) as Box<dyn Write>)
126        }));
127        destination.unopened = None;
128        destination.open = true;
129    }
130
131    /// Drop a destination [`redirect_on_first_write`](Self::redirect_on_first_write)
132    /// armed but never needed, so the run keeps the destination it had.
133    pub fn cancel_pending_redirect(&self) {
134        self.0.borrow_mut().pending = None;
135    }
136
137    /// False once a write met a `BrokenPipe`: what follows is discarded.
138    pub fn is_open(&self) -> bool {
139        self.0.borrow().open
140    }
141
142    pub fn with_writer<R>(&self, write: impl FnOnce(&mut dyn Write) -> R) -> R {
143        write(&mut *self.0.borrow_mut())
144    }
145
146    /// One write of `bytes` and the newline that terminates it, then a flush.
147    pub fn write_line(&self, bytes: &[u8]) -> std::io::Result<()> {
148        self.with_writer(|writer| {
149            writer.write_all(bytes)?;
150            writer.write_all(b"\n")?;
151            writer.flush()
152        })
153    }
154}
155
156#[derive(Clone, Debug, Default)]
157pub struct StreamCapture(Rc<RefCell<Vec<u8>>>);
158
159impl StreamCapture {
160    pub fn take(&self) -> Vec<u8> {
161        std::mem::take(&mut *self.0.borrow_mut())
162    }
163}
164
165impl Write for StreamCapture {
166    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
167        self.0.borrow_mut().extend_from_slice(buf);
168        Ok(buf.len())
169    }
170
171    fn flush(&mut self) -> std::io::Result<()> {
172        Ok(())
173    }
174}
175
176impl fmt::Debug for StreamSink {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        f.write_str("StreamSink")
179    }
180}
181
182#[cfg(test)]
183mod tests {
184    use super::*;
185
186    struct Closed;
187
188    impl Write for Closed {
189        fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
190            Err(std::io::Error::new(ErrorKind::BrokenPipe, "closed"))
191        }
192        fn flush(&mut self) -> std::io::Result<()> {
193            Ok(())
194        }
195    }
196
197    #[test]
198    fn a_sink_writes_one_line_per_call() {
199        let captured = StreamCapture::default();
200        let sink = StreamSink::new(captured.clone());
201        sink.write_line(b"{\"n\":1}").unwrap();
202        sink.write_line(b"{\"n\":2}").unwrap();
203        assert_eq!(captured.take(), b"{\"n\":1}\n{\"n\":2}\n");
204    }
205
206    #[test]
207    fn a_redirected_sink_moves_every_clone_to_the_new_destination() {
208        let first = StreamCapture::default();
209        let second = StreamCapture::default();
210        let sink = StreamSink::new(first.clone());
211        let clone = sink.clone();
212        clone.write_line(b"{\"n\":1}").unwrap();
213        sink.redirect(second.clone());
214        clone.write_line(b"{\"n\":2}").unwrap();
215        sink.with_writer(|w| w.write_all(b"tail\n")).unwrap();
216        assert_eq!(first.take(), b"{\"n\":1}\n");
217        assert_eq!(second.take(), b"{\"n\":2}\ntail\n");
218    }
219
220    #[test]
221    fn a_reader_that_left_closes_the_sink_and_every_later_write_succeeds() {
222        let sink = StreamSink::new(Closed);
223        assert!(sink.is_open());
224        sink.write_line(b"first").unwrap();
225        assert!(!sink.is_open());
226        sink.write_line(b"second").unwrap();
227        sink.with_writer(|w| writeln!(w, "third")).unwrap();
228    }
229
230    #[test]
231    fn a_deferred_destination_opens_on_the_first_write_and_not_before() {
232        let opened = Rc::new(RefCell::new(0));
233        let captured = StreamCapture::default();
234        let sink = StreamSink::new(Vec::new());
235        let count = opened.clone();
236        let target = captured.clone();
237        sink.redirect_on_first_write(move || {
238            *count.borrow_mut() += 1;
239            Ok(target)
240        });
241        assert_eq!(*opened.borrow(), 0);
242        sink.write_line(b"first").unwrap();
243        sink.write_line(b"second").unwrap();
244        assert_eq!(*opened.borrow(), 1);
245        assert_eq!(captured.take(), b"first\nsecond\n");
246    }
247
248    #[test]
249    fn a_deferred_destination_that_cannot_open_fails_the_write_that_needed_it() {
250        let sink = StreamSink::new(Vec::new());
251        sink.redirect_on_first_write(|| -> std::io::Result<Vec<u8>> {
252            Err(std::io::Error::new(
253                ErrorKind::NotFound,
254                "no such directory",
255            ))
256        });
257        let error = sink.write_line(b"first").unwrap_err();
258        assert_eq!(error.kind(), ErrorKind::NotFound);
259    }
260
261    #[test]
262    fn a_deferred_destination_that_cannot_open_keeps_failing_and_writes_nowhere() {
263        let before = StreamCapture::default();
264        let sink = StreamSink::new(before.clone());
265        sink.redirect_on_first_write(|| -> std::io::Result<Vec<u8>> {
266            Err(std::io::Error::new(
267                ErrorKind::NotFound,
268                "no such directory",
269            ))
270        });
271        assert_eq!(
272            sink.write_line(b"first").unwrap_err().kind(),
273            ErrorKind::NotFound
274        );
275        assert_eq!(
276            sink.write_line(b"second").unwrap_err().kind(),
277            ErrorKind::NotFound
278        );
279        assert_eq!(
280            sink.with_writer(|w| w.flush()).unwrap_err().kind(),
281            ErrorKind::NotFound
282        );
283        assert!(before.take().is_empty());
284    }
285
286    #[test]
287    fn a_write_failure_that_is_not_a_broken_pipe_is_reported() {
288        struct Full;
289        impl Write for Full {
290            fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
291                Err(std::io::Error::new(ErrorKind::StorageFull, "no room"))
292            }
293            fn flush(&mut self) -> std::io::Result<()> {
294                Ok(())
295            }
296        }
297        let sink = StreamSink::new(Full);
298        assert!(sink.write_line(b"first").is_err());
299        assert!(sink.is_open());
300    }
301}