如何为在pty下运行的进程设置终端前台进程组?
我写了一个简单的脚本,叫做 retry.py,用来在命令失败时重复执行这些命令。不过,我想看到子命令的输出,所以我不得不使用一些pty的小技巧。这在像rsync这样的程序上效果不错,但像scp这样的程序在显示进度条时会有额外的检查。
scp的代码有一个大致的测试:
getpgrp() == tcgetpgrp(STDOUT_FILENO);
当我通过这个脚本运行时,这个测试会失败。你可以看看我简单的tty_test.c测试案例:
./tty_tests
isatty reports 1
pgrps are 13619 and 13619
还有:
./retry.py -v -- ./tty_tests
command is ['./tty_tests']
isatty reports 1
pgrps are 13614 and -1
child finished: rc = 0
Ran command 1 times
我尝试使用tcsetpgrp(),这在pty文件描述符上会变成一个IOCTL操作,但这导致了一个EINVAL的错误。我更希望能继续使用Python的子进程功能,如果不行的话,是不是必须手动使用fork/execve来解决这个问题?
1 个回答
11
我觉得如果你不需要给子进程提供一个全新的伪终端(pty),可以把你的程序简化成这样:
from argparse import ArgumentParser
import os
import signal
import subprocess
import itertools
# your argumentparser stuff goes here
def become_tty_fg():
os.setpgrp()
hdlr = signal.signal(signal.SIGTTOU, signal.SIG_IGN)
tty = os.open('/dev/tty', os.O_RDWR)
os.tcsetpgrp(tty, os.getpgrp())
signal.signal(signal.SIGTTOU, hdlr)
if __name__ == "__main__":
args = parser.parse_args()
if args.verbose: print "command is %s" % (args.command)
if args.invert and args.limit==None:
sys.exit("You must define a limit if you have inverted the return code test")
for run_count in itertools.count():
return_code = subprocess.call(args.command, close_fds=True,
preexec_fn=become_tty_fg)
if args.test == True: break
if run_count >= args.limit: break
if args.invert and return_code != 0: break
elif not args.invert and return_code == 0: break
print "Ran command %d times" % (run_count)
这里的 setpgrp()
调用会在同一个会话中创建一个新的进程组,这样新的进程就能接收到用户的 ctrl-c、ctrl-z 等信号,而你的重试脚本则不会接收到这些信号。接着,tcsetpgrp()
会把这个新的进程组设置为控制终端的前台进程。当这个发生时,新的进程会收到一个 SIGTTOU
信号(因为自从调用了 setpgrp()
之后,它就处于一个后台进程组),通常这会导致进程停止,所以我们需要忽略 SIGTTOU
信号。我们把 SIGTTOU
的处理方式设置回之前的状态,以减少子进程因为意外信号而感到困惑的可能性。
现在,子进程已经在控制终端的前台组中,它的 tcgetpgrp()
和 getpgrp()
返回的值会是一样的,并且 isatty(1)
会返回真(假设它从 retry.py 继承的标准输出确实是一个终端)。你不需要在子进程和终端之间转发数据,这样就可以省去所有的 select
事件处理和非阻塞设置。