From 95838c66f92ac508a1afefd75c1531021eb34f8d Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Sun, 12 Jun 2022 14:10:18 -0700 Subject: [PATCH 1/4] Add `IsTerminal` trait to determine if a descriptor or handle is a terminal The UNIX and WASI implementations use `isatty`. The Windows implementation uses the same logic the `atty` crate uses, including the hack needed to detect msys terminals. Implement this trait for `File` and for `Stdin`/`Stdout`/`Stderr` and their locked counterparts on all platforms. On UNIX and WASI, implement it for `BorrowedFd`/`OwnedFd`. On Windows, implement it for `BorrowedHandle`/`OwnedHandle`. Based on https://github.com/rust-lang/rust/pull/91121 Co-authored-by: Matt Wilkinson --- library/std/src/io/mod.rs | 2 + library/std/src/io/stdio.rs | 29 ++++++++++++ library/std/src/lib.rs | 1 + library/std/src/os/fd/owned.rs | 17 +++++++ library/std/src/os/windows/io/handle.rs | 17 +++++++ library/std/src/sys/unix/io.rs | 6 +++ library/std/src/sys/unsupported/io.rs | 4 ++ library/std/src/sys/wasi/io.rs | 6 +++ library/std/src/sys/windows/c.rs | 8 ++++ library/std/src/sys/windows/io.rs | 59 +++++++++++++++++++++++++ library/test/src/cli.rs | 4 +- library/test/src/helpers/isatty.rs | 32 -------------- library/test/src/helpers/mod.rs | 1 - library/test/src/lib.rs | 1 + 14 files changed, 152 insertions(+), 35 deletions(-) delete mode 100644 library/test/src/helpers/isatty.rs diff --git a/library/std/src/io/mod.rs b/library/std/src/io/mod.rs index eeace2c43c4a4..8e3d330be0d99 100644 --- a/library/std/src/io/mod.rs +++ b/library/std/src/io/mod.rs @@ -265,6 +265,8 @@ pub use self::buffered::WriterPanicked; #[unstable(feature = "internal_output_capture", issue = "none")] #[doc(no_inline, hidden)] pub use self::stdio::set_output_capture; +#[unstable(feature = "is_terminal", issue = "98070")] +pub use self::stdio::IsTerminal; #[unstable(feature = "print_internals", issue = "none")] pub use self::stdio::{_eprint, _print}; #[stable(feature = "rust1", since = "1.0.0")] diff --git a/library/std/src/io/stdio.rs b/library/std/src/io/stdio.rs index 2dc12a18a8a66..3c3fa9654bbea 100644 --- a/library/std/src/io/stdio.rs +++ b/library/std/src/io/stdio.rs @@ -7,6 +7,7 @@ use crate::io::prelude::*; use crate::cell::{Cell, RefCell}; use crate::fmt; +use crate::fs::File; use crate::io::{self, BufReader, IoSlice, IoSliceMut, LineWriter, Lines}; use crate::sync::atomic::{AtomicBool, Ordering}; use crate::sync::{Arc, Mutex, MutexGuard, OnceLock}; @@ -1019,6 +1020,34 @@ where } } +/// Trait to determine if a descriptor/handle refers to a terminal/tty. +#[unstable(feature = "is_terminal", issue = "98070")] +pub trait IsTerminal: crate::sealed::Sealed { + /// Returns `true` if the descriptor/handle refers to a terminal/tty. + /// + /// On platforms where Rust does not know how to detect a terminal yet, this will return + /// `false`. This will also return `false` if an unexpected error occurred, such as from + /// passing an invalid file descriptor. + fn is_terminal(&self) -> bool; +} + +macro_rules! impl_is_terminal { + ($($t:ty),*$(,)?) => {$( + #[unstable(feature = "sealed", issue = "none")] + impl crate::sealed::Sealed for $t {} + + #[unstable(feature = "is_terminal", issue = "98070")] + impl IsTerminal for $t { + #[inline] + fn is_terminal(&self) -> bool { + crate::sys::io::is_terminal(self) + } + } + )*} +} + +impl_is_terminal!(File, Stdin, StdinLock<'_>, Stdout, StdoutLock<'_>, Stderr, StderrLock<'_>); + #[unstable( feature = "print_internals", reason = "implementation detail which may disappear or be replaced at any time", diff --git a/library/std/src/lib.rs b/library/std/src/lib.rs index a497acda4f60c..f5529b026ad92 100644 --- a/library/std/src/lib.rs +++ b/library/std/src/lib.rs @@ -253,6 +253,7 @@ #![feature(exhaustive_patterns)] #![feature(if_let_guard)] #![feature(intra_doc_pointers)] +#![feature(is_terminal)] #![feature(lang_items)] #![feature(let_chains)] #![feature(linkage)] diff --git a/library/std/src/os/fd/owned.rs b/library/std/src/os/fd/owned.rs index 9875c389d8aaf..732dfe22cab01 100644 --- a/library/std/src/os/fd/owned.rs +++ b/library/std/src/os/fd/owned.rs @@ -192,6 +192,23 @@ impl fmt::Debug for OwnedFd { } } +macro_rules! impl_is_terminal { + ($($t:ty),*$(,)?) => {$( + #[unstable(feature = "sealed", issue = "none")] + impl crate::sealed::Sealed for $t {} + + #[unstable(feature = "is_terminal", issue = "98070")] + impl crate::io::IsTerminal for $t { + #[inline] + fn is_terminal(&self) -> bool { + crate::sys::io::is_terminal(self) + } + } + )*} +} + +impl_is_terminal!(BorrowedFd<'_>, OwnedFd); + /// A trait to borrow the file descriptor from an underlying object. /// /// This is only available on unix platforms and must be imported in order to diff --git a/library/std/src/os/windows/io/handle.rs b/library/std/src/os/windows/io/handle.rs index 16cc8fa2783ee..1dfecc57338a7 100644 --- a/library/std/src/os/windows/io/handle.rs +++ b/library/std/src/os/windows/io/handle.rs @@ -384,6 +384,23 @@ impl fmt::Debug for OwnedHandle { } } +macro_rules! impl_is_terminal { + ($($t:ty),*$(,)?) => {$( + #[unstable(feature = "sealed", issue = "none")] + impl crate::sealed::Sealed for $t {} + + #[unstable(feature = "is_terminal", issue = "98070")] + impl crate::io::IsTerminal for $t { + #[inline] + fn is_terminal(&self) -> bool { + crate::sys::io::is_terminal(self) + } + } + )*} +} + +impl_is_terminal!(BorrowedHandle<'_>, OwnedHandle); + /// A trait to borrow the handle from an underlying object. #[stable(feature = "io_safety", since = "1.63.0")] pub trait AsHandle { diff --git a/library/std/src/sys/unix/io.rs b/library/std/src/sys/unix/io.rs index deb5ee76bd035..29c340dd34942 100644 --- a/library/std/src/sys/unix/io.rs +++ b/library/std/src/sys/unix/io.rs @@ -1,4 +1,5 @@ use crate::marker::PhantomData; +use crate::os::fd::{AsFd, AsRawFd}; use crate::slice; use libc::{c_void, iovec}; @@ -74,3 +75,8 @@ impl<'a> IoSliceMut<'a> { unsafe { slice::from_raw_parts_mut(self.vec.iov_base as *mut u8, self.vec.iov_len) } } } + +pub fn is_terminal(fd: &impl AsFd) -> bool { + let fd = fd.as_fd(); + unsafe { libc::isatty(fd.as_raw_fd()) != 0 } +} diff --git a/library/std/src/sys/unsupported/io.rs b/library/std/src/sys/unsupported/io.rs index d5f475b4310fd..82610ffab7e1e 100644 --- a/library/std/src/sys/unsupported/io.rs +++ b/library/std/src/sys/unsupported/io.rs @@ -45,3 +45,7 @@ impl<'a> IoSliceMut<'a> { self.0 } } + +pub fn is_terminal(_: &T) -> bool { + false +} diff --git a/library/std/src/sys/wasi/io.rs b/library/std/src/sys/wasi/io.rs index ee017d13a4ca0..2cd45df88fad1 100644 --- a/library/std/src/sys/wasi/io.rs +++ b/library/std/src/sys/wasi/io.rs @@ -1,6 +1,7 @@ #![deny(unsafe_op_in_unsafe_fn)] use crate::marker::PhantomData; +use crate::os::fd::{AsFd, AsRawFd}; use crate::slice; #[derive(Copy, Clone)] @@ -71,3 +72,8 @@ impl<'a> IoSliceMut<'a> { unsafe { slice::from_raw_parts_mut(self.vec.buf as *mut u8, self.vec.buf_len) } } } + +pub fn is_terminal(fd: &impl AsFd) -> bool { + let fd = fd.as_fd(); + unsafe { libc::isatty(fd.as_raw_fd()) != 0 } +} diff --git a/library/std/src/sys/windows/c.rs b/library/std/src/sys/windows/c.rs index c61a7e7d1e4ab..3f59abb5b0ec3 100644 --- a/library/std/src/sys/windows/c.rs +++ b/library/std/src/sys/windows/c.rs @@ -126,6 +126,8 @@ pub const SECURITY_SQOS_PRESENT: DWORD = 0x00100000; pub const FIONBIO: c_ulong = 0x8004667e; +pub const MAX_PATH: usize = 260; + #[repr(C)] #[derive(Copy)] pub struct WIN32_FIND_DATAW { @@ -534,6 +536,12 @@ pub struct SYMBOLIC_LINK_REPARSE_BUFFER { /// NB: Use carefully! In general using this as a reference is likely to get the /// provenance wrong for the `PathBuffer` field! +#[repr(C)] +pub struct FILE_NAME_INFO { + pub FileNameLength: DWORD, + pub FileName: [WCHAR; 1], +} + #[repr(C)] pub struct MOUNT_POINT_REPARSE_BUFFER { pub SubstituteNameOffset: c_ushort, diff --git a/library/std/src/sys/windows/io.rs b/library/std/src/sys/windows/io.rs index fb06df1f80cda..489d66b06714f 100644 --- a/library/std/src/sys/windows/io.rs +++ b/library/std/src/sys/windows/io.rs @@ -1,6 +1,10 @@ use crate::marker::PhantomData; +use crate::mem::size_of; +use crate::os::windows::io::{AsHandle, AsRawHandle, BorrowedHandle}; use crate::slice; use crate::sys::c; +use core; +use libc; #[derive(Copy, Clone)] #[repr(transparent)] @@ -78,3 +82,58 @@ impl<'a> IoSliceMut<'a> { unsafe { slice::from_raw_parts_mut(self.vec.buf as *mut u8, self.vec.len as usize) } } } + +pub fn is_terminal(h: &impl AsHandle) -> bool { + unsafe { handle_is_console(h.as_handle()) } +} + +unsafe fn handle_is_console(handle: BorrowedHandle<'_>) -> bool { + let handle = handle.as_raw_handle(); + + let mut out = 0; + if c::GetConsoleMode(handle, &mut out) != 0 { + // False positives aren't possible. If we got a console then we definitely have a console. + return true; + } + + // At this point, we *could* have a false negative. We can determine that this is a true + // negative if we can detect the presence of a console on any of the standard I/O streams. If + // another stream has a console, then we know we're in a Windows console and can therefore + // trust the negative. + for std_handle in [c::STD_INPUT_HANDLE, c::STD_OUTPUT_HANDLE, c::STD_ERROR_HANDLE] { + let std_handle = c::GetStdHandle(std_handle); + if std_handle != handle && c::GetConsoleMode(std_handle, &mut out) != 0 { + return false; + } + } + + // Otherwise, we fall back to an msys hack to see if we can detect the presence of a pty. + msys_tty_on(handle) +} + +unsafe fn msys_tty_on(handle: c::HANDLE) -> bool { + let size = size_of::() + c::MAX_PATH * size_of::(); + let mut name_info_bytes = vec![0u8; size]; + let res = c::GetFileInformationByHandleEx( + handle, + c::FileNameInfo, + name_info_bytes.as_mut_ptr() as *mut libc::c_void, + size as u32, + ); + if res == 0 { + return false; + } + let name_info: &c::FILE_NAME_INFO = &*(name_info_bytes.as_ptr() as *const c::FILE_NAME_INFO); + let s = core::slice::from_raw_parts( + name_info.FileName.as_ptr(), + name_info.FileNameLength as usize / 2, + ); + let name = String::from_utf16_lossy(s); + // This checks whether 'pty' exists in the file name, which indicates that + // a pseudo-terminal is attached. To mitigate against false positives + // (e.g., an actual file name that contains 'pty'), we also require that + // either the strings 'msys-' or 'cygwin-' are in the file name as well.) + let is_msys = name.contains("msys-") || name.contains("cygwin-"); + let is_pty = name.contains("-pty"); + is_msys && is_pty +} diff --git a/library/test/src/cli.rs b/library/test/src/cli.rs index f981b9c495476..8be32183fe780 100644 --- a/library/test/src/cli.rs +++ b/library/test/src/cli.rs @@ -3,9 +3,9 @@ use std::env; use std::path::PathBuf; -use super::helpers::isatty; use super::options::{ColorConfig, Options, OutputFormat, RunIgnored}; use super::time::TestTimeOptions; +use std::io::{self, IsTerminal}; #[derive(Debug)] pub struct TestOpts { @@ -32,7 +32,7 @@ pub struct TestOpts { impl TestOpts { pub fn use_color(&self) -> bool { match self.color { - ColorConfig::AutoColor => !self.nocapture && isatty::stdout_isatty(), + ColorConfig::AutoColor => !self.nocapture && io::stdout().is_terminal(), ColorConfig::AlwaysColor => true, ColorConfig::NeverColor => false, } diff --git a/library/test/src/helpers/isatty.rs b/library/test/src/helpers/isatty.rs deleted file mode 100644 index 874ecc3764572..0000000000000 --- a/library/test/src/helpers/isatty.rs +++ /dev/null @@ -1,32 +0,0 @@ -//! Helper module which provides a function to test -//! if stdout is a tty. - -cfg_if::cfg_if! { - if #[cfg(unix)] { - pub fn stdout_isatty() -> bool { - unsafe { libc::isatty(libc::STDOUT_FILENO) != 0 } - } - } else if #[cfg(windows)] { - pub fn stdout_isatty() -> bool { - type DWORD = u32; - type BOOL = i32; - type HANDLE = *mut u8; - type LPDWORD = *mut u32; - const STD_OUTPUT_HANDLE: DWORD = -11i32 as DWORD; - extern "system" { - fn GetStdHandle(which: DWORD) -> HANDLE; - fn GetConsoleMode(hConsoleHandle: HANDLE, lpMode: LPDWORD) -> BOOL; - } - unsafe { - let handle = GetStdHandle(STD_OUTPUT_HANDLE); - let mut out = 0; - GetConsoleMode(handle, &mut out) != 0 - } - } - } else { - // FIXME: Implement isatty on SGX - pub fn stdout_isatty() -> bool { - false - } - } -} diff --git a/library/test/src/helpers/mod.rs b/library/test/src/helpers/mod.rs index 049cadf86a6d0..6f366a911e8cd 100644 --- a/library/test/src/helpers/mod.rs +++ b/library/test/src/helpers/mod.rs @@ -3,6 +3,5 @@ pub mod concurrency; pub mod exit_code; -pub mod isatty; pub mod metrics; pub mod shuffle; diff --git a/library/test/src/lib.rs b/library/test/src/lib.rs index c30257fc79200..b1e0bbfc591c1 100644 --- a/library/test/src/lib.rs +++ b/library/test/src/lib.rs @@ -17,6 +17,7 @@ #![unstable(feature = "test", issue = "50297")] #![doc(test(attr(deny(warnings))))] #![feature(internal_output_capture)] +#![feature(is_terminal)] #![feature(staged_api)] #![feature(process_exitcode_internals)] #![feature(test)] From 516577d87f28fd92f5947b25e21c64e1557f3f47 Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Sun, 19 Jun 2022 18:11:39 -0700 Subject: [PATCH 2/4] Make is_terminal fail fast if a process has no console at all If a process has no console, it'll have NULL in place of a console handle, so return early with `false` in that case without making any OS calls. --- library/std/src/sys/windows/io.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/library/std/src/sys/windows/io.rs b/library/std/src/sys/windows/io.rs index 489d66b06714f..0ec619315633e 100644 --- a/library/std/src/sys/windows/io.rs +++ b/library/std/src/sys/windows/io.rs @@ -90,6 +90,11 @@ pub fn is_terminal(h: &impl AsHandle) -> bool { unsafe fn handle_is_console(handle: BorrowedHandle<'_>) -> bool { let handle = handle.as_raw_handle(); + // A null handle means the process has no console. + if handle.is_null() { + return false; + } + let mut out = 0; if c::GetConsoleMode(handle, &mut out) != 0 { // False positives aren't possible. If we got a console then we definitely have a console. @@ -102,7 +107,10 @@ unsafe fn handle_is_console(handle: BorrowedHandle<'_>) -> bool { // trust the negative. for std_handle in [c::STD_INPUT_HANDLE, c::STD_OUTPUT_HANDLE, c::STD_ERROR_HANDLE] { let std_handle = c::GetStdHandle(std_handle); - if std_handle != handle && c::GetConsoleMode(std_handle, &mut out) != 0 { + if !std_handle.is_null() + && std_handle != handle + && c::GetConsoleMode(std_handle, &mut out) != 0 + { return false; } } From af470ad838c5bb45cdb119fdc12aaca82063b0d7 Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Wed, 24 Aug 2022 15:32:16 +0200 Subject: [PATCH 3/4] Rewrite FILE_NAME_INFO handling to avoid enlarging slice reference Rather than referencing a slice's pointer and then creating a new slice with a longer length, offset from the base structure pointer instead. This makes some choices of Rust semantics happier. --- library/std/src/sys/windows/io.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/library/std/src/sys/windows/io.rs b/library/std/src/sys/windows/io.rs index 0ec619315633e..7e7518b6d8905 100644 --- a/library/std/src/sys/windows/io.rs +++ b/library/std/src/sys/windows/io.rs @@ -132,10 +132,10 @@ unsafe fn msys_tty_on(handle: c::HANDLE) -> bool { return false; } let name_info: &c::FILE_NAME_INFO = &*(name_info_bytes.as_ptr() as *const c::FILE_NAME_INFO); - let s = core::slice::from_raw_parts( - name_info.FileName.as_ptr(), - name_info.FileNameLength as usize / 2, - ); + let name_len = name_info.FileNameLength as usize / 2; + // Offset to get the `FileName` field. + let name_ptr = name_info_bytes.as_ptr().offset(size_of::() as isize).cast::(); + let s = core::slice::from_raw_parts(name_ptr, name_len); let name = String::from_utf16_lossy(s); // This checks whether 'pty' exists in the file name, which indicates that // a pseudo-terminal is attached. To mitigate against false positives From ef009b6558c0d52a9fc0b2ca064a0c18c00c4226 Mon Sep 17 00:00:00 2001 From: Josh Triplett Date: Fri, 9 Sep 2022 15:01:26 +0200 Subject: [PATCH 4/4] Use Align8 to avoid misalignment if the allocator or Vec doesn't align allocations --- library/std/src/sys/windows/io.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/library/std/src/sys/windows/io.rs b/library/std/src/sys/windows/io.rs index 7e7518b6d8905..0879ef199338e 100644 --- a/library/std/src/sys/windows/io.rs +++ b/library/std/src/sys/windows/io.rs @@ -2,7 +2,7 @@ use crate::marker::PhantomData; use crate::mem::size_of; use crate::os::windows::io::{AsHandle, AsRawHandle, BorrowedHandle}; use crate::slice; -use crate::sys::c; +use crate::sys::{c, Align8}; use core; use libc; @@ -120,21 +120,21 @@ unsafe fn handle_is_console(handle: BorrowedHandle<'_>) -> bool { } unsafe fn msys_tty_on(handle: c::HANDLE) -> bool { - let size = size_of::() + c::MAX_PATH * size_of::(); - let mut name_info_bytes = vec![0u8; size]; + const SIZE: usize = size_of::() + c::MAX_PATH * size_of::(); + let mut name_info_bytes = Align8([0u8; SIZE]); let res = c::GetFileInformationByHandleEx( handle, c::FileNameInfo, - name_info_bytes.as_mut_ptr() as *mut libc::c_void, - size as u32, + name_info_bytes.0.as_mut_ptr() as *mut libc::c_void, + SIZE as u32, ); if res == 0 { return false; } - let name_info: &c::FILE_NAME_INFO = &*(name_info_bytes.as_ptr() as *const c::FILE_NAME_INFO); + let name_info: &c::FILE_NAME_INFO = &*(name_info_bytes.0.as_ptr() as *const c::FILE_NAME_INFO); let name_len = name_info.FileNameLength as usize / 2; // Offset to get the `FileName` field. - let name_ptr = name_info_bytes.as_ptr().offset(size_of::() as isize).cast::(); + let name_ptr = name_info_bytes.0.as_ptr().offset(size_of::() as isize).cast::(); let s = core::slice::from_raw_parts(name_ptr, name_len); let name = String::from_utf16_lossy(s); // This checks whether 'pty' exists in the file name, which indicates that