如何消除伪终端执行命令后的额外按键需求
伪终端运行外部命令后挂起,需额外按键才恢复
动机
希望在程序中通过伪终端运行包含复杂终端操作的外部命令,外部命令结束后立即恢复程序控制权。
问题现象
程序运行基本正常,但外部命令退出后会挂起,必须按下回车、ESC、空格等任意按键才会继续执行后续代码。
最小复现代码
使用ls | less作为测试命令的最小复现代码:
import os, pty, termios, sys, select, tty from subprocess import Popen original_tty_settings = termios.tcgetattr(sys.stdin) tty.setraw(sys.stdin.fileno()) try: master_fd, slave_fd = pty.openpty() p = Popen( 'ls | less', preexec_fn = os.setsid, stdin = slave_fd, stdout = slave_fd, stderr = slave_fd, universal_newlines = True, shell = True, ) while p.poll() is None: r, w, e = select.select([sys.stdin, master_fd], [], []) if sys.stdin in r: data_in = os.read(sys.stdin.fileno(), 1024) os.write(master_fd, data_in) elif master_fd in r: data_out = os.read(master_fd, 1024) if data_out: os.write(sys.stdout.fileno(), data_out) finally: termios.tcsetattr(sys.stdin, termios.TCSADRAIN, original_tty_settings) print('Resuming')
预期与实际结果
- 预期结果:运行
ls | less后,按下q退出less,程序立即打印Resuming并退出。 - 实际情况:按下
q后程序无响应,需额外按键才会继续执行。
已尝试方案
- 将所有文件描述符加入
select的异常列表(xlist) - 为
Popen添加close_fds=True参数 - 尝试将阻塞式
os.read()改为非阻塞,但未成功
问题根源
当子进程(less)退出后,master_fd会进入EOF状态,但原代码的select.select()会一直阻塞,等待sys.stdin或master_fd的可读事件。此时没有键盘输入的话,select不会返回,循环也无法检查到子进程已退出,直到有按键触发select返回,才会退出循环。
解决方案
修改代码,通过添加select超时定期检查子进程状态,同时检测master_fd的EOF信号,及时终止循环:
import os, pty, termios, sys, select, tty from subprocess import Popen original_tty_settings = termios.tcgetattr(sys.stdin) tty.setraw(sys.stdin.fileno()) try: master_fd, slave_fd = pty.openpty() p = Popen( 'ls | less', preexec_fn = os.setsid, stdin = slave_fd, stdout = slave_fd, stderr = slave_fd, universal_newlines = True, shell = True, ) while True: # 先检查子进程是否已终止,避免select长期阻塞 exit_code = p.poll() if exit_code is not None: break # 添加0.1秒超时,定期唤醒select以检查进程状态 r, w, e = select.select([sys.stdin, master_fd], [], [], 0.1) if sys.stdin in r: data_in = os.read(sys.stdin.fileno(), 1024) os.write(master_fd, data_in) elif master_fd in r: data_out = os.read(master_fd, 1024) if not data_out: # 读到EOF,说明伪终端slave端已关闭,子进程终止 break os.write(sys.stdout.fileno(), data_out) finally: termios.tcsetattr(sys.stdin, termios.TCSADRAIN, original_tty_settings) print('Resuming')
修复说明
- 添加
select超时:设置0.1秒超时,让循环定期唤醒,检查子进程状态,避免子进程退出后select一直阻塞。 - 检测
master_fd的EOF:当读取master_fd得到空数据时,直接跳出循环,因为这表示子进程已关闭伪终端连接。 - 调整循环逻辑:将原循环条件改为
while True,在每次循环开头先检查子进程状态,确保子进程退出后能及时终止循环。
内容的提问来源于stack exchange,提问作者Jan Spurny
相关产品推荐
相关产品推荐

