You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Rust启动PTY报错:无法设置终端进程组及回显问题

解决Rust PTY Shell的终端错误与回显问题

问题描述

运行Rust PTY Shell程序时出现两个核心问题:

  • 终端报错:bash: cannot set terminal process group (14080): Inappropriate ioctl for device
  • 命令回显:输入的命令会被重复输出,无法关闭

原代码及运行输出如下:

原代码

use std::process::{Command, Stdio};
use std::os::fd::{FromRawFd};
use nix::pty::{openpty};
use nix::unistd::{ read, write };

fn launch_pty(shell: &str) -> i32 {
    let ends = openpty(None, None).expect("Failed to open pty");
    let master = ends.master;
    let slave = ends.slave;

    let mut command = Command::new(shell);
    command.stdin(unsafe { Stdio::from_raw_fd(slave) });
    command.stdout(unsafe { Stdio::from_raw_fd(slave) });
    command.stderr(unsafe { Stdio::from_raw_fd(slave) });

    match command.spawn()  {
        Ok(_) => {
            master
        },
        Err(e) => {
            panic!("Failed to create pty: {}", e);
        }
    }
}

fn main() {
    let pty = launch_pty("/bin/bash");
    write(pty, b"uname -r\n").unwrap();
    
    loop {
        let mut buffer = [0; 4096];
        match read(pty, &mut buffer) {
            Ok(bytes_read) => {
                println!("{}", String::from_utf8(buffer[0..bytes_read].to_vec()).unwrap());
            },
            Err(_) => {
                break;
            }
        }
    }
}

原运行输出

uname -r
bash: cannot set terminal process group (14080): Inappropriate ioctl for device
bash: no job control in this shell
mahmoud@DESKTOP-MOT06CH:~/Documents/rust_terminal$ uname -r
5.15.90.1-microsoft-standard-WSL2

解决方案

错误原因分析

  1. 终端进程组错误:bash需要将自身设为终端的前台进程组,但当前slave PTY未成为子进程的控制终端,导致ioctl调用失败。
  2. 命令回显:PTY默认开启回显模式,输入字符会被终端驱动自动回显,需手动关闭该属性。

修复后的代码

use std::process::{Command, Stdio};
use std::os::fd::{FromRawFd, OwnedFd};
use nix::pty::{openpty};
use nix::unistd::{read, write, setsid, fork, ForkResult};
use nix::sys::termios::{tcgetattr, tcsetattr, Termios, ECHO, ICANON};
use nix::sys::ioctl::{ioctl, TIOCSCTTY};
use nix::errno::Errno;

fn launch_pty(shell: &str) -> OwnedFd {
    let ends = openpty(None, None).expect("Failed to open pty");
    let master = ends.master;
    let slave = ends.slave;

    // 复制slave fd,避免所有权冲突
    let slave_clone = slave.try_clone().expect("Failed to clone slave fd");

    match fork().expect("Failed to fork process") {
        ForkResult::Parent { child: _ } => {
            // 父进程关闭slave fd,仅保留master用于通信
            drop(slave);
            master
        },
        ForkResult::Child => {
            // 子进程成为会话首进程,脱离原控制终端
            setsid().expect("Failed to set session id");
            
            // 将slave PTY设置为子进程的控制终端
            unsafe {
                ioctl(slave_clone.as_raw_fd(), TIOCSCTTY, 0)
                    .expect("Failed to set controlling terminal");
            }

            // 获取终端属性,关闭回显与规范模式
            let mut termios = tcgetattr(slave_clone.as_raw_fd()).expect("Failed to get termios");
            termios.c_lflag &= !(ECHO | ICANON); // 关闭回显和按行输入的规范模式
            tcsetattr(slave_clone.as_raw_fd(), nix::sys::termios::SetArg::TCSANOW, &termios)
                .expect("Failed to set termios");

            // 重定向标准IO到slave PTY
            std::io::stdin().set_raw_fd(slave_clone.as_raw_fd()).unwrap();
            std::io::stdout().set_raw_fd(slave_clone.as_raw_fd()).unwrap();
            std::io::stderr().set_raw_fd(slave_clone.as_raw_fd()).unwrap();

            // 启动shell并等待其退出
            Command::new(shell)
                .spawn()
                .expect("Failed to spawn shell")
                .wait()
                .expect("Failed to wait for shell");
            std::process::exit(0);
        }
    }
}

fn main() {
    let pty = launch_pty("/bin/bash");
    write(pty.as_raw_fd(), b"uname -r\n").unwrap();
    
    loop {
        let mut buffer = [0; 4096];
        match read(pty.as_raw_fd(), &mut buffer) {
            Ok(bytes_read) if bytes_read > 0 => {
                print!("{}", String::from_utf8_lossy(&buffer[0..bytes_read]));
            },
            Ok(_) => break,
            Err(e) if e == Errno::EIO => break, // PTY slave关闭时触发EIO错误,退出循环
            Err(e) => panic!("Read error: {}", e),
        }
    }
}

关键修改点

  • 通过fork()创建独立子进程,在子进程中执行setsid()成为会话首进程,确保能获取控制终端
  • 用TIOCSCTTY ioctl将slave PTY设为子进程的控制终端,解决bash进程组错误
  • 修改终端属性,关闭ECHO(回显)和ICANON(规范模式),消除命令回显
  • 使用OwnedFd管理文件描述符所有权,避免资源泄漏与安全问题
  • 父进程关闭slave fd,仅保留master用于通信
  • 读取输出时用print!替代println!,避免额外换行,同时处理EIO错误(slave关闭时触发)

修复后运行输出

5.15.90.1-microsoft-standard-WSL2

内容的提问来源于stack exchange,提问作者Mhhany

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.17 02:17:32