Skip to main content

term_sys_io/
redirect_stdio.rs

1//! Redirect OS-level file descriptors (stdout/stderr) into `tracing`.
2//!
3//! macOS system frameworks (AppKit, NSPasteboard) and C libraries often write
4//! debug output directly to FD 1 or 2.  When the terminal is in raw/alt-screen
5//! mode this junk leaks to the display.  These helpers pipe the FD through a
6//! background thread into `tracing`.  (The `StderrSuppressGuard` — a
7//! short-lived null-device redirect for `arboard` — lives in this crate's
8//! sibling `stderr_suppress` module.)
9
10use std::io::BufRead;
11
12/// Redirect an OS-level file descriptor into a callback.
13///
14/// Spawns a background thread that reads from the FD and calls `on_line`
15/// for each non-empty line.  Non-UTF-8 bytes are handled via
16/// `String::from_utf8_lossy`.
17///
18/// - **Unix**: creates a pipe, uses `dup2` to redirect the FD.
19/// - **Windows**: creates a Win32 anonymous pipe, redirects both the CRT
20///   descriptor and the Win32 handle.
21#[cfg(unix)]
22pub fn redirect_fd<F>(target_fd: libc::c_int, on_line: F) -> std::io::Result<()>
23where
24    F: Fn(&str) + Send + 'static,
25{
26    let mut fds: [libc::c_int; 2] = [0; 2];
27    unsafe {
28        if libc::pipe(fds.as_mut_ptr()) == -1 {
29            return Err(std::io::Error::last_os_error());
30        }
31        if libc::dup2(fds[1], target_fd) == -1 {
32            libc::close(fds[0]);
33            libc::close(fds[1]);
34            return Err(std::io::Error::last_os_error());
35        }
36        libc::close(fds[1]);
37    }
38    let read_fd = fds[0];
39    std::thread::Builder::new()
40        .name("fd-redirect".into())
41        .spawn(move || {
42            use std::os::unix::io::FromRawFd;
43            let file = unsafe { std::fs::File::from_raw_fd(read_fd) };
44            let mut reader = std::io::BufReader::new(file);
45            let mut buf = Vec::new();
46            while reader.read_until(b'\n', &mut buf).unwrap_or(0) > 0 {
47                let text = String::from_utf8_lossy(&buf);
48                let trimmed = text.trim();
49                if !trimmed.is_empty() {
50                    on_line(trimmed);
51                }
52                buf.clear();
53            }
54        })?;
55    Ok(())
56}
57
58/// Windows implementation — same semantics as the Unix version.
59#[cfg(windows)]
60pub fn redirect_fd<F>(target_fd: i32, on_line: F) -> std::io::Result<()>
61where
62    F: Fn(&str) + Send + 'static,
63{
64    use std::os::windows::io::FromRawHandle;
65
66    unsafe extern "system" {
67        fn SetStdHandle(nStdHandle: u32, hHandle: isize) -> i32;
68        fn CreatePipe(
69            hReadPipe: *mut isize,
70            hWritePipe: *mut isize,
71            lpPipeAttributes: *const std::ffi::c_void,
72            nSize: u32,
73        ) -> i32;
74    }
75
76    const STD_ERROR_HANDLE: u32 = 0xFFFFFFF4u32;
77    const STD_OUTPUT_HANDLE: u32 = 0xFFFFFFF5u32;
78
79    let win_std_handle = if target_fd == 1 {
80        STD_OUTPUT_HANDLE
81    } else {
82        STD_ERROR_HANDLE
83    };
84
85    unsafe {
86        let mut read_handle: isize = 0;
87        let mut write_handle: isize = 0;
88
89        if CreatePipe(&mut read_handle, &mut write_handle, std::ptr::null(), 0) == 0 {
90            return Err(std::io::Error::last_os_error());
91        }
92
93        // Redirect the Win32 handle
94        SetStdHandle(win_std_handle, write_handle);
95
96        // Redirect the CRT file descriptor
97        let write_fd = libc::open_osfhandle(write_handle, 0);
98        if write_fd != -1 {
99            libc::dup2(write_fd, target_fd);
100        }
101
102        let file = std::fs::File::from_raw_handle(read_handle as _);
103
104        std::thread::Builder::new()
105            .name("fd-redirect".into())
106            .spawn(move || {
107                let mut reader = std::io::BufReader::new(file);
108                let mut buf = Vec::new();
109                while reader.read_until(b'\n', &mut buf).unwrap_or(0) > 0 {
110                    let text = String::from_utf8_lossy(&buf);
111                    let trimmed = text.trim();
112                    if !trimmed.is_empty() {
113                        on_line(trimmed);
114                    }
115                    buf.clear();
116                }
117            })?;
118    }
119
120    Ok(())
121}
122
123/// Convenience wrapper: redirects an FD and feeds lines into `tracing`.
124#[cfg(any(unix, windows))]
125pub fn redirect_fd_to_tracing(target_fd: impl Into<i32>, is_stderr: bool) -> std::io::Result<()> {
126    let target_fd = target_fd.into();
127    if is_stderr {
128        redirect_fd(target_fd, |line| {
129            tracing::error!(target: "c_stderr", "{}", line);
130        })
131    } else {
132        redirect_fd(target_fd, |line| {
133            tracing::info!(target: "c_stdout", "{}", line);
134        })
135    }
136}
137
138/// No-op fallback for unsupported platforms (e.g. wasm).
139#[cfg(not(any(unix, windows)))]
140pub fn redirect_fd_to_tracing(_target_fd: i32, _is_stderr: bool) -> std::io::Result<()> {
141    Ok(())
142}
143
144/// No-op fallback.
145#[cfg(not(any(unix, windows)))]
146pub fn redirect_fd<F>(_target_fd: i32, _on_line: F) -> std::io::Result<()>
147where
148    F: Fn(&str) + Send + 'static,
149{
150    Ok(())
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156    use std::sync::{Arc, Mutex};
157
158    // ── redirect_fd_to_tracing ────────────────────────────────────
159
160    #[test]
161    #[cfg(any(unix, windows))]
162    fn redirect_fd_captures_stdout_and_stderr() {
163        #[cfg(unix)]
164        let (stdout_fd, stderr_fd) = {
165            let mut a: [libc::c_int; 2] = [0; 2];
166            let mut b: [libc::c_int; 2] = [0; 2];
167            unsafe {
168                assert_eq!(libc::pipe(a.as_mut_ptr()), 0);
169                assert_eq!(libc::pipe(b.as_mut_ptr()), 0);
170            }
171            (a[1], b[1])
172        };
173        #[cfg(windows)]
174        let (stdout_fd, stderr_fd) = {
175            unsafe extern "system" {
176                fn CreatePipe(
177                    h: *mut isize,
178                    w: *mut isize,
179                    a: *const std::ffi::c_void,
180                    s: u32,
181                ) -> i32;
182            }
183            let mut ra = 0isize;
184            let mut wa = 0isize;
185            let mut rb = 0isize;
186            let mut wb = 0isize;
187            unsafe {
188                assert_ne!(CreatePipe(&mut ra, &mut wa, std::ptr::null(), 0), 0);
189                assert_ne!(CreatePipe(&mut rb, &mut wb, std::ptr::null(), 0), 0);
190            }
191            let a = unsafe { libc::open_osfhandle(wa, 0) };
192            let b = unsafe { libc::open_osfhandle(wb, 0) };
193            assert!(a != -1 && b != -1);
194            (a, b)
195        };
196
197        let stdout_lines = Arc::new(Mutex::new(Vec::new()));
198        let stderr_lines = Arc::new(Mutex::new(Vec::new()));
199
200        {
201            let out = Arc::clone(&stdout_lines);
202            redirect_fd(stdout_fd, move |line| {
203                out.lock().unwrap().push(line.to_string())
204            })
205            .expect("redirect stdout");
206        }
207        {
208            let err = Arc::clone(&stderr_lines);
209            redirect_fd(stderr_fd, move |line| {
210                err.lock().unwrap().push(line.to_string())
211            })
212            .expect("redirect stderr");
213        }
214
215        unsafe {
216            libc::write(stdout_fd, c"hello from stdout\n".as_ptr().cast(), 18);
217            libc::write(stderr_fd, c"hello from stderr\n".as_ptr().cast(), 18);
218        }
219
220        #[cfg(unix)]
221        unsafe {
222            libc::close(stdout_fd);
223            libc::close(stderr_fd);
224        }
225        #[cfg(windows)]
226        unsafe {
227            libc::close(stdout_fd);
228            libc::close(stderr_fd);
229        }
230
231        std::thread::sleep(std::time::Duration::from_millis(100));
232
233        let stdout: Vec<_> = stdout_lines.lock().unwrap().clone();
234        let stderr: Vec<_> = stderr_lines.lock().unwrap().clone();
235        assert!(
236            stdout.iter().any(|l| l.contains("hello from stdout")),
237            "stdout: got {stdout:?}"
238        );
239        assert!(
240            stderr.iter().any(|l| l.contains("hello from stderr")),
241            "stderr: got {stderr:?}"
242        );
243    }
244}