如何从伪终端仅读取stdout/stderr(排除stdin输入内容)并解决sudo的终端校验问题
如何从伪终端仅读取stdout/stderr(排除stdin输入内容)并解决sudo的终端校验问题
嗨,我来帮你理清这个问题的核心,以及给出可行的解决方案。
首先,你的第一个版本代码里,从master pty读到输入内容(比如输入ls后会把ls本身也读出来),原因是伪终端默认开启了本地回显模式——终端驱动会把你输入的内容自动回显到输出流里,所以从master fd读取时会包含你输入的命令文本,而不仅仅是程序的输出。
然后你第二个版本改了管道,结果sudo报错,这是因为你破坏了伪终端的关联:forkpty本来已经把子进程的stdin/stdout/stderr绑定到了pty的slave端(这是一个真正的终端设备),但你用dup2把它们换成了管道,而管道不是终端,sudo会检测isatty(STDIN_FILENO)来判断是否有终端可以读取密码,自然就报错了。
正确的解决方案思路
要同时满足两个需求:
- 只读取程序的stdout/stderr,不包含输入的回显内容
- 让子进程保持真正的终端关联,确保sudo能正常工作
核心是保留forkpty创建的pty关联,通过控制终端属性关闭回显,而不是用管道替换标准流。
修正后的代码示例
#include <stdio.h> #include <unistd.h> #include <termios.h> #include <util.h> #include <iostream> int main(int argc, char** args) { // 错误处理lambda auto err = [](const char* msg) { std::cout << msg << "\n"; return 1; }; int pid, ptyfd; char buf[512]; const int buffsize = 512; // 用forkpty创建伪终端:子进程的stdin/stdout/stderr会自动绑定到pty slave端 if ((pid = ::forkpty(&ptyfd, NULL, NULL, NULL)) > 0) { // 关键:关闭master pty的本地回显,这样输入内容不会被回显到输出 struct termios term_attr; if (tcgetattr(ptyfd, &term_attr) == -1) { return err("Failed to get terminal attributes"); } term_attr.c_lflag &= ~ECHO; // 清除ECHO标志,禁用本地回显 if (tcsetattr(ptyfd, TCSANOW, &term_attr) == -1) { return err("Failed to set terminal attributes"); } // 启动一个子进程,负责将用户输入转发到master pty(给zsh的stdin) if (fork() == 0) { while (true) { int nread = ::read(STDIN_FILENO, buf, buffsize); if (nread < 0) { return err("Error reading user input"); } else if (nread == 0) { std::cout << "Input stream closed\n"; break; } // 将输入写入master pty,发送给zsh if (::write(ptyfd, buf, nread) != nread) { return err("Failed to write to pty"); } } exit(0); } // 主进程负责读取master pty的输出(zsh的stdout/stderr) while (true) { int nread = ::read(ptyfd, buf, buffsize); if (nread <= 0) { std::cout << "Zsh session ended\n"; break; } // 将输出打印到当前终端 if (::write(STDOUT_FILENO, buf, nread) != nread) { return err("Failed to write to stdout"); } } } else if (pid == 0) { // 子进程直接执行zsh,stdin/stdout/stderr已经是pty slave,无需修改 execl("/bin/zsh", "zsh", "-l", "-i", NULL); // 如果exec失败,退出 exit(1); } else { return err("forkpty failed"); } return 0; }
关键细节解释
- forkpty的正确使用:forkpty会自动完成所有pty关联工作——子进程的标准流直接绑定到pty的slave端,这是一个真正的终端设备,所以sudo会认为有可用的终端来读取密码,不会报错。
- 关闭本地回显:通过
tcgetattr获取终端属性,清除ECHO标志后用tcsetattr设置,这样输入的内容不会被终端回显到输出流,你从master fd读取的就只有zsh的真正输出(命令结果、错误信息等),不会包含你输入的命令文本。 - 输入输出转发:用一个单独的子进程处理用户输入到pty的转发,主进程处理pty输出到当前终端的转发,这样交互逻辑清晰,同时保持pty的完整关联。
为什么你的第二个版本会失败?
你在第二个版本里用dup2把子进程的stdin/stdout绑定到了管道,这相当于把zsh的标准流从pty slave换成了管道。而管道不是终端设备,当sudo调用isatty(STDIN_FILENO)时会返回false,所以它会报错提示需要终端。
备注:内容来源于stack exchange,提问作者user7934593
相关产品推荐
相关产品推荐

