Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add IsTerminal trait to determine if a descriptor or handle is a terminal #98033

Merged
merged 4 commits into from
Oct 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions library/std/src/io/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ pub(crate) use self::stdio::attempt_print_to_stderr;
#[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")]
Expand Down
29 changes: 29 additions & 0 deletions library/std/src/io/stdio.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -1035,6 +1036,34 @@ pub(crate) fn attempt_print_to_stderr(args: fmt::Arguments<'_>) {
let _ = stderr().write_fmt(args);
}

/// 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",
Expand Down
1 change: 1 addition & 0 deletions library/std/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
17 changes: 17 additions & 0 deletions library/std/src/os/fd/owned.rs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,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
Expand Down
17 changes: 17 additions & 0 deletions library/std/src/os/windows/io/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions library/std/src/sys/unix/io.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::marker::PhantomData;
use crate::os::fd::{AsFd, AsRawFd};
use crate::slice;

use libc::{c_void, iovec};
Expand Down Expand Up @@ -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 }
}
4 changes: 4 additions & 0 deletions library/std/src/sys/unsupported/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,7 @@ impl<'a> IoSliceMut<'a> {
self.0
}
}

pub fn is_terminal<T>(_: &T) -> bool {
false
}
6 changes: 6 additions & 0 deletions library/std/src/sys/wasi/io.rs
Original file line number Diff line number Diff line change
@@ -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)]
Expand Down Expand Up @@ -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 }
}
8 changes: 8 additions & 0 deletions library/std/src/sys/windows/c.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,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 {
Expand Down Expand Up @@ -538,6 +540,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,
Expand Down
69 changes: 68 additions & 1 deletion library/std/src/sys/windows/io.rs
Original file line number Diff line number Diff line change
@@ -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 crate::sys::{c, Align8};
use core;
use libc;

#[derive(Copy, Clone)]
#[repr(transparent)]
Expand Down Expand Up @@ -78,3 +82,66 @@ 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();

// 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.
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.is_null()
&& 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 {
const SIZE: usize = size_of::<c::FILE_NAME_INFO>() + c::MAX_PATH * size_of::<c::WCHAR>();
let mut name_info_bytes = Align8([0u8; SIZE]);
let res = c::GetFileInformationByHandleEx(
handle,
c::FileNameInfo,
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.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.0.as_ptr().offset(size_of::<c::DWORD>() as isize).cast::<u16>();
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
// (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
}
4 changes: 2 additions & 2 deletions library/test/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
}
Expand Down
32 changes: 0 additions & 32 deletions library/test/src/helpers/isatty.rs

This file was deleted.

1 change: 0 additions & 1 deletion library/test/src/helpers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,5 @@

pub mod concurrency;
pub mod exit_code;
pub mod isatty;
pub mod metrics;
pub mod shuffle;
1 change: 1 addition & 0 deletions library/test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down