term_sys_io/
redirect_stdio.rs1use std::io::BufRead;
11
12#[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#[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 SetStdHandle(win_std_handle, write_handle);
95
96 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#[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#[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#[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 #[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}