use crate::Error;
#[derive(Debug)]
pub struct TtyHandles {
pub tty_in: std::fs::File,
pub tty_out: std::fs::File,
}
#[derive(Debug)]
pub struct PreservedStdout {
inner: std::fs::File,
}
impl PreservedStdout {
pub fn as_writer(&mut self) -> &mut std::fs::File {
&mut self.inner
}
}
pub fn open_controlling_tty() -> Result<TtyHandles, Error> {
if test_fail_tty_enabled() {
return Err(Error::NoControllingTty);
}
#[cfg(unix)]
{
unix::open_tty()
}
#[cfg(windows)]
{
windows_impl::open_tty()
}
}
fn test_fail_tty_enabled() -> bool {
match std::env::var_os("RUSTY_VIPE_TEST_FAIL_TTY") {
Some(v) => match v.to_str() {
Some(s) => matches!(
s.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
),
None => false,
},
None => false,
}
}
pub fn preserve_stdout() -> Result<PreservedStdout, Error> {
#[cfg(unix)]
{
unix::preserve_stdout()
}
#[cfg(windows)]
{
windows_impl::preserve_stdout()
}
}
#[cfg(unix)]
mod unix {
use super::{Error, PreservedStdout, TtyHandles};
use std::os::fd::{FromRawFd, OwnedFd};
use std::os::unix::io::AsRawFd;
pub fn open_tty() -> Result<TtyHandles, Error> {
let tty_in = std::fs::OpenOptions::new()
.read(true)
.open("/dev/tty")
.map_err(|_| Error::NoControllingTty)?;
let tty_out = std::fs::OpenOptions::new()
.write(true)
.open("/dev/tty")
.map_err(|_| Error::NoControllingTty)?;
Ok(TtyHandles { tty_in, tty_out })
}
pub fn preserve_stdout() -> Result<PreservedStdout, Error> {
let stdout = std::io::stdout();
let raw_fd = stdout.as_raw_fd();
let new_fd = unsafe { libc::dup(raw_fd) };
if new_fd < 0 {
return Err(Error::Io(std::io::Error::last_os_error()));
}
let owned: OwnedFd = unsafe { OwnedFd::from_raw_fd(new_fd) };
Ok(PreservedStdout {
inner: std::fs::File::from(owned),
})
}
}
#[cfg(windows)]
mod windows_impl {
use super::{Error, PreservedStdout, TtyHandles};
use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle};
use windows_sys::Win32::Foundation::{
DUPLICATE_SAME_ACCESS, DuplicateHandle, HANDLE, INVALID_HANDLE_VALUE,
};
use windows_sys::Win32::Storage::FileSystem::{
CreateFileW, FILE_GENERIC_READ, FILE_GENERIC_WRITE, FILE_SHARE_READ, FILE_SHARE_WRITE,
OPEN_EXISTING,
};
use windows_sys::Win32::System::Threading::GetCurrentProcess;
fn wide(s: &str) -> Vec<u16> {
s.encode_utf16().chain(std::iter::once(0)).collect()
}
fn create_file(name: &str, access: u32) -> Result<HANDLE, Error> {
let wide_name = wide(name);
let handle = unsafe {
CreateFileW(
wide_name.as_ptr(),
access,
FILE_SHARE_READ | FILE_SHARE_WRITE,
std::ptr::null(),
OPEN_EXISTING,
0,
std::ptr::null_mut(),
)
};
if handle == INVALID_HANDLE_VALUE || handle.is_null() {
return Err(Error::NoControllingTty);
}
Ok(handle)
}
pub fn open_tty() -> Result<TtyHandles, Error> {
let conin = create_file("CONIN$", FILE_GENERIC_READ)?;
let conout = create_file("CONOUT$", FILE_GENERIC_WRITE)?;
let owned_in: OwnedHandle = unsafe { OwnedHandle::from_raw_handle(conin as _) };
let owned_out: OwnedHandle = unsafe { OwnedHandle::from_raw_handle(conout as _) };
Ok(TtyHandles {
tty_in: std::fs::File::from(owned_in),
tty_out: std::fs::File::from(owned_out),
})
}
pub fn preserve_stdout() -> Result<PreservedStdout, Error> {
let stdout = std::io::stdout();
let raw = stdout.as_raw_handle();
let mut duplicate: HANDLE = std::ptr::null_mut();
let ok = unsafe {
DuplicateHandle(
GetCurrentProcess(),
raw as _,
GetCurrentProcess(),
&mut duplicate,
0,
0,
DUPLICATE_SAME_ACCESS,
)
};
if ok == 0 {
return Err(Error::Io(std::io::Error::last_os_error()));
}
let owned: OwnedHandle = unsafe { OwnedHandle::from_raw_handle(duplicate as _) };
Ok(PreservedStdout {
inner: std::fs::File::from(owned),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn open_controlling_tty_returns_no_tty_or_handles() {
match open_controlling_tty() {
Ok(_handles) => {}
Err(Error::NoControllingTty) => {}
Err(other) => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn preserve_stdout_succeeds_in_normal_process() {
let _preserved = preserve_stdout().expect("preserve_stdout should succeed");
}
}