1use std::fmt;
17use std::future::Future;
18use std::io::{self, Write};
19use std::sync::{Arc, Mutex, RwLock};
20
21pub type Sink = Arc<Mutex<dyn Write + Send>>;
23
24static SINK: RwLock<Option<Sink>> = RwLock::new(None);
26
27pub fn notice(args: fmt::Arguments<'_>) {
32 let _ = Stderr.write_fmt(format_args!("{}\n", args));
33}
34
35tokio::task_local! {
36 static SCOPED: Sink;
38}
39
40pub fn scoped<F: Future>(sink: Sink, fut: F) -> impl Future<Output = F::Output> {
46 SCOPED.scope(sink, fut)
47}
48
49pub fn redirect(sink: Sink) -> RedirectGuard {
54 let mut slot = SINK.write().unwrap_or_else(|p| p.into_inner());
55 let previous = slot.replace(sink);
56 RedirectGuard { previous }
57}
58
59#[must_use = "diagnostics return to stderr as soon as the guard is dropped"]
61pub struct RedirectGuard {
62 previous: Option<Sink>,
63}
64
65impl Drop for RedirectGuard {
66 fn drop(&mut self) {
67 let mut slot = SINK.write().unwrap_or_else(|p| p.into_inner());
68 *slot = self.previous.take();
69 }
70}
71
72#[derive(Debug, Clone, Copy, Default)]
77pub struct Stderr;
78
79impl Write for Stderr {
80 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
81 if let Ok(written) =
82 SCOPED.try_with(|sink| sink.lock().unwrap_or_else(|p| p.into_inner()).write(buf))
83 {
84 return written;
85 }
86 let slot = SINK.read().unwrap_or_else(|p| p.into_inner());
87 match slot.as_ref() {
88 Some(sink) => sink.lock().unwrap_or_else(|p| p.into_inner()).write(buf),
89 None => io::stderr().write(buf),
90 }
91 }
92
93 fn flush(&mut self) -> io::Result<()> {
94 if let Ok(flushed) =
95 SCOPED.try_with(|sink| sink.lock().unwrap_or_else(|p| p.into_inner()).flush())
96 {
97 return flushed;
98 }
99 let slot = SINK.read().unwrap_or_else(|p| p.into_inner());
100 match slot.as_ref() {
101 Some(sink) => sink.lock().unwrap_or_else(|p| p.into_inner()).flush(),
102 None => io::stderr().flush(),
103 }
104 }
105}
106
107#[macro_export]
109macro_rules! notice {
110 ($($arg:tt)*) => {
111 $crate::diag::notice(format_args!($($arg)*))
112 };
113}
114
115#[cfg(test)]
120mod tests {
121 use super::*;
122
123 static REDIRECT_LOCK: Mutex<()> = Mutex::new(());
127
128 #[test]
129 fn test_stderr_writer_flushes_with_and_without_redirect() {
130 let _lock = REDIRECT_LOCK.lock().unwrap_or_else(|p| p.into_inner());
131 let mut w = Stderr;
133 assert!(w.flush().is_ok());
134 let buf: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
135 let _g = redirect(buf.clone());
136 assert!(w.flush().is_ok());
137 }
138
139 #[test]
140 fn test_a_scoped_future_keeps_its_notices_from_the_redirect() {
141 let _lock = REDIRECT_LOCK.lock().unwrap_or_else(|p| p.into_inner());
142 let rt = tokio::runtime::Builder::new_current_thread()
144 .build()
145 .unwrap();
146 let pane: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
149 let watch: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
150 let _g = redirect(pane.clone());
151 let watching = scoped(watch.clone(), async {
152 for i in 0..3 {
153 notice(format_args!("watch {i}"));
154 tokio::task::yield_now().await;
155 }
156 });
157 let command = async {
158 for i in 0..3 {
159 notice(format_args!("pane {i}"));
160 tokio::task::yield_now().await;
161 }
162 };
163 rt.block_on(async { tokio::join!(watching, command) });
164 let pane = String::from_utf8_lossy(&pane.lock().unwrap()).into_owned();
165 let watch = String::from_utf8_lossy(&watch.lock().unwrap()).into_owned();
166 assert!(
167 watch.contains(
168 "watch 0
169watch 1
170watch 2
171"
172 ),
173 "{watch}"
174 );
175 assert!(!watch.contains("pane"), "{watch}");
176 assert!(
177 pane.contains(
178 "pane 0
179"
180 ) && pane.contains(
181 "pane 2
182"
183 ),
184 "{pane}"
185 );
186 assert!(!pane.contains("watch"), "{pane}");
187 }
188
189 #[test]
190 fn test_redirect_captures_and_restores() {
191 let _lock = REDIRECT_LOCK.lock().unwrap_or_else(|p| p.into_inner());
192 let outer: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
195 let inner: Arc<Mutex<Vec<u8>>> = Arc::new(Mutex::new(Vec::new()));
196 {
197 let _g1 = redirect(outer.clone());
198 notice(format_args!("one"));
199 {
200 let _g2 = redirect(inner.clone());
201 crate::notice!("two {}", 2);
202 }
203 notice(format_args!("three"));
204 }
205 let outer = String::from_utf8_lossy(&outer.lock().unwrap()).into_owned();
208 let inner = String::from_utf8_lossy(&inner.lock().unwrap()).into_owned();
209 let (one, three) = (outer.find("one\n").unwrap(), outer.find("three\n").unwrap());
210 assert!(one < three);
211 assert!(!outer.contains("two 2"));
212 assert!(inner.contains("two 2\n"));
213 assert!(!inner.contains("one") && !inner.contains("three"));
214 }
215}