首页 > 其他分享 >短小精悍(2) - Rust终端检测库is-terminal和atty介绍

短小精悍(2) - Rust终端检测库is-terminal和atty介绍

时间:2023-12-23 21:55:06浏览次数:48  
标签:handle name stdout atty terminal 终端 msys Rust

title: 短小精悍(2) - Rust终端检测库is-terminal和atty介绍
zhihu-url: https://zhuanlan.zhihu.com/p/673841498

今天给大家介绍的是Rust中非常常用的两个用于检测终端的库is-terminalatty。这两个库都是千万级别的下载量,大多数和日志、流、交互相关的库都会依赖它们,而我们在开发基础工具时可能也会用到。

由于两个包的功能都大差不差,因此接下来主要介绍is-terminal

简介

两个库的文档都很简单:该库用于回答一个简单的问题:这是不是终端(tty)?

image-20231223192224840

但是,可能有读者还在疑惑:到底什么是终端?程序不在终端运行,还能在哪里运行?关于tty和终端究竟是什么,可以参考这个知乎问题;但是在这里,终端是相对于普通文件流的一个概念,可以参考下面的图片来理解:

is-terminal(1)

我们可以看到,在右边的测试中,我们将输出重定向到了一个txt文件里,is-terminal就对stdout给出了“不是终端”的判断。类似的测试对管道也是成立的,如果我们把输入或输出使用管道进行了重定向,is-terminal也会给出“不是终端”的判断。

当程序被其他程序调用并作为新进程运行时,is-terminal的判断则取决于我们怎么处理它的stdout。比如下面是另一个Rust程序的代码,它创建了一个检查stdout是否在终端内的进程,并收集其输出(位于stderr):

use std::{process::Command, io, fs::File};

fn main() {
    Command::new("./rust-is-terminal").stderr(io::stdout()).spawn().unwrap();
}

我们并没有收集它的stdout,因此它判断它的stdout位于终端内:

稍加修改:我们将它的stdout重定向到一个文件中。

use std::{process::Command, io, fs::File};

fn main() {
    let stdout = File::create("out.txt").unwrap();
    Command::new("./rust-is-terminal").stdout(stdout).stderr(io::stdout()).spawn().unwrap();
}

这次运行,它判断它的stdout不在终端内:

用途

所以这玩意儿有什么用呢?其实它的用处还真挺大。首先,我们知道终端和普通的文件流都是计算机中用于数据输入和输出的接口,但它们在功能和用途上存在显著差异。例如,终端可以解释特殊的控制序列,像Linux终端里大名鼎鼎的\033\x1b),便可以移动光标、清屏、设置颜色等。因此,程序在运行时就可以通过判断流是否来自终端,来决定自己要不要提供仅在终端下可用的功能。

比如下面的代码,它在终端内运行时可以获得非常漂亮的输出效果:

fn main() {
    println!("\x1b[0;30;41m WARN \x1b[0m The target is not found \n")
}

但是假如这个程序需要被定期自动执行,管理员在检查程序输出的时候就会被难以阅读的控制序列所困扰:

因此,这段代码更加合理的写法便是:

use is_terminal::IsTerminal;
use std::io::stdout;

fn main() {
    if stdout().is_terminal() {
        println!("\x1b[0;30;41m WARN \x1b[0m The target is not found \n");
    }else{
        println!("[ WARN ] The target is not found \n");
    }
}

扩展:实现原理

如果你是因为想知道is-terminalatty怎么用/有什么用而点进这篇文章的话,那么读到这里对你来说应该已经差不多了。不过,如果你对它的实现原理感兴趣的话,我们接下来可以一起研究一下。

Windows平台

在Windows上,is-terminal使用了一个叫做GetConsoleMode的Windows API来判断一个流是否属于终端。这个API的作用是检索控制台的输入模式/屏幕缓冲区的输出模式。具体的文档可以参见这里

我们简单地读一下这个API的文档,就会发现这个API实际上本意并非用于判断终端,而是用于获取当前终端的各种属性,例如终端当前是否位于插入模式、是否位于快速编辑模式等等。那么如果提供API的流不属于终端呢?这就会导致GetConsoleMode出错,并返回一个代表错误的0。

以下摘自is-terminal的源码:

#[cfg(windows)]
fn handle_is_console(handle: BorrowedHandle<'_>) -> bool {
    use windows_sys::Win32::System::Console::{
        GetConsoleMode, GetStdHandle, STD_ERROR_HANDLE, STD_INPUT_HANDLE, STD_OUTPUT_HANDLE,
    };

    let handle = handle.as_raw_handle();

    unsafe {
        // A null handle means the process has no console.
        if handle.is_null() {
            return false;
        }

        let mut out = 0;
        if GetConsoleMode(handle as 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 [STD_INPUT_HANDLE, STD_OUTPUT_HANDLE, STD_ERROR_HANDLE] {
            let std_handle = GetStdHandle(std_handle);
            if std_handle != 0
                && std_handle != handle as HANDLE
                && 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 as HANDLE)
    }
}

值得一提的是,is-terminal的作者注意到了在Windows上利用这个API来判断流是否来自终端并不总是可靠,比如当程序在msys或cygwin的环境中运行时,即使它确确实实位于msys的终端下,Windows也会认为它并没有在终端内运行。作者在注释中称这种特性为假阴性。因此,为了排除这种假阴性,作者首先检查了另外两个标准流是否来自终端,只要它们中有一个被检查出来自终端,就说明程序确实是运行在终端而不是msys下。如果另外两个标准流也并非来自于终端,那么程序就会检查流是否来自于msys的终端。

检查一个流是否来自msys终端的原理也很简单:作者调用了另一个API GetFileInformationByHandleEx,这个API的作用是检索句柄对应的文件信息。作者利用这个API来获取流的文件名,如果文件名带有msys-cygwin-的前缀和-pty的后缀,就能够证明流来自于msys或cygwin的终端。

以下是这一过程的源码:

#[cfg(windows)]
unsafe fn msys_tty_on(handle: HANDLE) -> bool {
    use std::ffi::c_void;
    use windows_sys::Win32::{
        Foundation::MAX_PATH,
        Storage::FileSystem::{
            FileNameInfo, GetFileInformationByHandleEx, GetFileType, FILE_TYPE_PIPE,
        },
    };

    // Early return if the handle is not a pipe.
    if GetFileType(handle) != FILE_TYPE_PIPE {
        return false;
    }

    /// Mirrors windows_sys::Win32::Storage::FileSystem::FILE_NAME_INFO, giving
    /// it a fixed length that we can stack allocate
    #[repr(C)]
    #[allow(non_snake_case)]
    struct FILE_NAME_INFO {
        FileNameLength: u32,
        FileName: [u16; MAX_PATH as usize],
    }
    let mut name_info = FILE_NAME_INFO {
        FileNameLength: 0,
        FileName: [0; MAX_PATH as usize],
    };
    // Safety: buffer length is fixed.
    let res = GetFileInformationByHandleEx(
        handle,
        FileNameInfo,
        &mut name_info as *mut _ as *mut c_void,
        std::mem::size_of::<FILE_NAME_INFO>() as u32,
    );
    if res == 0 {
        return false;
    }

    // Use `get` because `FileNameLength` can be out of range.
    let s = match name_info
        .FileName
        .get(..name_info.FileNameLength as usize / 2)
    {
        None => return false,
        Some(s) => s,
    };
    let name = String::from_utf16_lossy(s);
    // Get the file name only.
    let name = name.rsplit('\\').next().unwrap_or(&name);
    // 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
    // the file name begins with either the strings 'msys-' or 'cygwin-'.)
    let is_msys = name.starts_with("msys-") || name.starts_with("cygwin-");
    let is_pty = name.contains("-pty");
    is_msys && is_pty
}

Unix/Linux平台

Linux和Unix这边就没那么多弯弯绕了。POSIX标准直接定义了接口isatty,任何兼容POSIX标准的操作系统都可以用这个接口来判断一个流是否来自于终端。

isatty(定义)

isatty(3) - Linux manual page

一个疑惑:Windows也是兼容POSIX接口的操作系统之一,为什么is-terminal的实现在GetConsoleMode的那一步里不使用Windows提供的_isatty接口呢?_isatty | Microsoft Learn

标签:handle,name,stdout,atty,terminal,终端,msys,Rust
From: https://www.cnblogs.com/cinea/p/17923707.html

相关文章

  • 2023最新高级难度Rust面试题,包含答案。刷题必备!记录一下。
    好记性不如烂笔头内容来自面试宝典-高级难度Rust面试题合集问:请解释Rust中的并行计算模型和分布式计算模型。在Rust中,你可以利用语言的并发特性来实现并行计算和分布式计算。虽然这些概念是不同的,但它们可以一起使用以提高系统的性能和扩展性。并行计算并行计算是......
  • beautify-windows-terminal
    title:美化WindowsTerminaldate:2021-2-278:42:48author:TokisakiGalaxyexcerpt:美化WindowsTerminaltags:-美化-WindowsTerminalcategories:软件安装与配置安装WindowsTerminal有两种下载方法,去Github或者WindowsStore。建议去WindowsStore下,可以自......
  • clion,rustrover,gdb,lldb设置调试汇编语法格式
    通过修改GDB的配置来改变显示的汇编代码的格式在用户目录(C:\Users\你的用户名)下创建一个.gdbinit文件,然后在该文件中添加以下内容:setdisassembly-flavorintel这行命令会将GDB的汇编指令格式设置为Intel格式如果你在调试过程中使用的是LLDB,你可以在.lldbinit文件中添加以下......
  • 2023最新初级难度Rust面试题,包含答案。刷题必备!记录一下。
    好记性不如烂笔头内容来自面试宝典-初级难度Rust面试题合集问:什么是Rust?它有什么优点?Rust是一种系统编程语言,由Mozilla在2006年开始开发,并于2010年首次发布。它的设计目标是提供安全、并发和高效的语言特性。Rust的语法与C和C++类似,但引入了许多创新的概念......
  • 2023最新中级难度Rust面试题,包含答案。刷题必备!记录一下。
    好记性不如烂笔头内容来自面试宝典-中级难度Rust面试题合集问:请解释Rust中的闭包捕获机制。在Rust中,闭包(closures)是一种可以捕获其创建环境中的变量的匿名函数。它们允许你定义一个临时的一次性函数,可以在任何地方使用,并且能够访问外部作用域内的数据。闭包有三种捕......
  • 36. 干货系列从零用Rust编写负载均衡及代理,内网穿透中内网代理的实现
    wmproxywmproxy已用Rust实现http/https代理,socks5代理,反向代理,静态文件服务器,四层TCP/UDP转发,七层负载均衡,内网穿透,后续将实现websocket代理等,会将实现过程分享出来,感兴趣的可以一起造个轮子项目地址国内:https://gitee.com/tickbh/wmproxygithub:https://github.com/......
  • Rust全局变量
    Rust全局变量Rust全局变量的一种解决方案,由于mut问题,会导致unsafe代码块。usestd::{collections::HashMap,sync::OnceLock};///全局变量,模拟SESSION管理器pubstaticmutSESSION:OnceLock<HashMap<String,String>>=OnceLock::new();///初始化函数pubfninit()......
  • rust语言_学习笔记
    rust语言_学习笔记转载注明来源:本文链接来自osnosn的博客,写于2023-12-10.安装rust【安装_rustup_cargo_rustc_交叉编译测试】cargo的config设置更换ustc源,使用代理。设置缺省registry。见【rustcargo配置】。crate库搜索去【crates.io】搜索去【docs.......
  • uniffi-rs rust 多语言bindings 生成工具
    uniffi-rs是基于webidl描述定义,然后生成不同语言bindings的工具,此工具是在学习pyo3的maturin工具看到的,整理记录下参考玩法 目前支持的语言官方支持的包含了Kotlin,Swift,Python,Ruby当然还有不少社区的实现,比如支持C#以及golang说明以上就是一个简单的记录,后边尝试......
  • 35. 干货系列从零用Rust编写负载均衡及代理,代理服务器的源码升级改造
    wmproxywmproxy已用Rust实现http/https代理,socks5代理,反向代理,静态文件服务器,四层TCP/UDP转发,七层负载均衡,内网穿透,后续将实现websocket代理等,会将实现过程分享出来,感兴趣的可以一起造个轮子项目地址国内:https://gitee.com/tickbh/wmproxygithub:https://github.com/......