Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I set the terminal foreground process group for a process I'm running under a pty?

I've written a simple wrapper script for repeating commands when they fail called retry.py. However as I want to see the output of child command I've had to pull some pty tricks. This works OK for programs like rsync but others like scp apply additional test for showing things like their progress meter.

The scp code has a test that is broadly:

getpgrp() == tcgetpgrp(STDOUT_FILENO);

Which fails when I run though the wrapper script. As you can see with my simple tty_test.c test case:

./tty_tests
isatty reports 1
pgrps are 13619 and 13619

and:

./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

I've tried using the tcsetpgrp() which ends up as an IOCTL on the pty fd's but that results in an -EINVAL for ptys. I'd prefer to keep using the Python subprocess machinery if at all possible or is manually fork/execve'ing going to be required for this?

like image 767
stsquad Avatar asked Mar 04 '13 11:03

stsquad


1 Answers

I believe you can pare your program down to this, if you don't need to provide a whole new pty to the subprocess:

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)

The setpgrp() call creates a new process group in the same session, so that the new process will receive any ctrl-c/ctrl-z/etc from the user, and your retry script won't. Then the tcsetpgrp() makes the new process group be the foreground one on the controlling tty. The new process gets a SIGTTOU when that happens (because since the setpgrp(), it has been in a background process group), which normally would make the process stop, so that's the reason for ignoring SIGTTOU. We set the SIGTTOU handler back to whatever it was before, to minimize the chance of the subprocess being confused by an unexpected signal table.

Since the subprocess is now in the foreground group for the tty, its tcgetpgrp() and getpgrp() will be the same, and isatty(1) will be true (assuming the stdout it inherits from retry.py actually is a tty). You don't need to proxy traffic between the subprocess and the tty, which lets you ditch all the select event handling and fcntl-nonblocking-setting.

like image 51
the paul Avatar answered Oct 14 '22 09:10

the paul