如何为在pty下运行的进程设置终端前台进程组?

7 投票
1 回答
1776 浏览
提问于 2025-04-17 17:51

我写了一个简单的脚本,叫做 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 事件处理和非阻塞设置。

撰写回答