1use crate::Error;
14
15#[derive(Debug)]
19pub struct TtyHandles {
20 pub tty_in: std::fs::File,
21 pub tty_out: std::fs::File,
22}
23
24#[derive(Debug)]
27pub struct PreservedStdout {
28 inner: std::fs::File,
29}
30
31impl PreservedStdout {
32 pub fn as_writer(&mut self) -> &mut std::fs::File {
34 &mut self.inner
35 }
36}
37
38pub fn open_controlling_tty() -> Result<TtyHandles, Error> {
49 if test_fail_tty_enabled() {
50 return Err(Error::NoControllingTty);
51 }
52 #[cfg(unix)]
53 {
54 unix::open_tty()
55 }
56 #[cfg(windows)]
57 {
58 windows_impl::open_tty()
59 }
60}
61
62fn test_fail_tty_enabled() -> bool {
65 match std::env::var_os("RUSTY_VIPE_TEST_FAIL_TTY") {
66 Some(v) => match v.to_str() {
67 Some(s) => matches!(
68 s.trim().to_ascii_lowercase().as_str(),
69 "1" | "true" | "yes" | "on"
70 ),
71 None => false,
72 },
73 None => false,
74 }
75}
76
77pub fn preserve_stdout() -> Result<PreservedStdout, Error> {
81 #[cfg(unix)]
82 {
83 unix::preserve_stdout()
84 }
85 #[cfg(windows)]
86 {
87 windows_impl::preserve_stdout()
88 }
89}
90
91#[cfg(unix)]
92mod unix {
93 use super::{Error, PreservedStdout, TtyHandles};
94 use std::os::fd::{FromRawFd, OwnedFd};
95 use std::os::unix::io::AsRawFd;
96
97 pub fn open_tty() -> Result<TtyHandles, Error> {
98 let tty_in = std::fs::OpenOptions::new()
99 .read(true)
100 .open("/dev/tty")
101 .map_err(|_| Error::NoControllingTty)?;
102 let tty_out = std::fs::OpenOptions::new()
103 .write(true)
104 .open("/dev/tty")
105 .map_err(|_| Error::NoControllingTty)?;
106 Ok(TtyHandles { tty_in, tty_out })
107 }
108
109 pub fn preserve_stdout() -> Result<PreservedStdout, Error> {
110 let stdout = std::io::stdout();
111 let raw_fd = stdout.as_raw_fd();
112 let new_fd = unsafe { libc::dup(raw_fd) };
118 if new_fd < 0 {
119 return Err(Error::Io(std::io::Error::last_os_error()));
120 }
121 let owned: OwnedFd = unsafe { OwnedFd::from_raw_fd(new_fd) };
124 Ok(PreservedStdout {
125 inner: std::fs::File::from(owned),
126 })
127 }
128}
129
130#[cfg(windows)]
131mod windows_impl {
132 use super::{Error, PreservedStdout, TtyHandles};
133 use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle};
134 use windows_sys::Win32::Foundation::{
135 DUPLICATE_SAME_ACCESS, DuplicateHandle, HANDLE, INVALID_HANDLE_VALUE,
136 };
137 use windows_sys::Win32::Storage::FileSystem::{
138 CreateFileW, FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_SHARE_READ, FILE_SHARE_WRITE,
139 OPEN_EXISTING,
140 };
141 use windows_sys::Win32::System::Threading::GetCurrentProcess;
142
143 fn wide(s: &str) -> Vec<u16> {
144 s.encode_utf16().chain(std::iter::once(0)).collect()
145 }
146
147 fn create_file(name: &str, access: u32) -> Result<HANDLE, Error> {
148 let wide_name = wide(name);
149 let handle = unsafe {
152 CreateFileW(
153 wide_name.as_ptr(),
154 access,
155 FILE_SHARE_READ | FILE_SHARE_WRITE,
156 std::ptr::null(),
157 OPEN_EXISTING,
158 0,
159 std::ptr::null_mut(),
160 )
161 };
162 if handle == INVALID_HANDLE_VALUE || handle.is_null() {
163 return Err(Error::NoControllingTty);
164 }
165 Ok(handle)
166 }
167
168 pub fn open_tty() -> Result<TtyHandles, Error> {
169 let conin = create_file("CONIN$", FILE_GENERIC_READ)?;
170 let conout = create_file("CONOUT$", FILE_GENERIC_WRITE)?;
171 let owned_in: OwnedHandle = unsafe { OwnedHandle::from_raw_handle(conin as _) };
173 let owned_out: OwnedHandle = unsafe { OwnedHandle::from_raw_handle(conout as _) };
174 Ok(TtyHandles {
175 tty_in: std::fs::File::from(owned_in),
176 tty_out: std::fs::File::from(owned_out),
177 })
178 }
179
180 pub fn preserve_stdout() -> Result<PreservedStdout, Error> {
181 let stdout = std::io::stdout();
182 let raw = stdout.as_raw_handle();
183 let mut duplicate: HANDLE = std::ptr::null_mut();
184 let ok = unsafe {
188 DuplicateHandle(
189 GetCurrentProcess(),
190 raw as _,
191 GetCurrentProcess(),
192 &mut duplicate,
193 0,
194 0,
195 DUPLICATE_SAME_ACCESS,
196 )
197 };
198 if ok == 0 {
199 return Err(Error::Io(std::io::Error::last_os_error()));
200 }
201 let owned: OwnedHandle = unsafe { OwnedHandle::from_raw_handle(duplicate as _) };
203 Ok(PreservedStdout {
204 inner: std::fs::File::from(owned),
205 })
206 }
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212
213 #[test]
214 fn open_controlling_tty_returns_no_tty_or_handles() {
215 match open_controlling_tty() {
220 Ok(_handles) => {}
221 Err(Error::NoControllingTty) => {}
222 Err(other) => panic!("unexpected error: {other:?}"),
223 }
224 }
225
226 #[test]
227 fn preserve_stdout_succeeds_in_normal_process() {
228 let _preserved = preserve_stdout().expect("preserve_stdout should succeed");
231 }
232}