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
解决方案
错误原因分析
- 终端进程组错误:bash需要将自身设为终端的前台进程组,但当前slave PTY未成为子进程的控制终端,导致ioctl调用失败。
- 命令回显: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()成为会话首进程,确保能获取控制终端 - 用
TIOCSCTTYioctl将slave PTY设为子进程的控制终端,解决bash进程组错误 - 修改终端属性,关闭
ECHO(回显)和ICANON(规范模式),消除命令回显 - 使用
OwnedFd管理文件描述符所有权,避免资源泄漏与安全问题 - 父进程关闭slave fd,仅保留master用于通信
- 读取输出时用
print!替代println!,避免额外换行,同时处理EIO错误(slave关闭时触发)
修复后运行输出
5.15.90.1-microsoft-standard-WSL2
内容的提问来源于stack exchange,提问作者Mhhany
相关产品推荐
相关产品推荐

