standout_dispatch/
stream.rs1use 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 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 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 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 pub fn cancel_pending_redirect(&self) {
134 self.0.borrow_mut().pending = None;
135 }
136
137 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 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}