How do I set the terminal foreground process group for a process I'm running under a pty?
Question: I've written a simple wrapper script for repeating commands when they fail
called [retry.py](https://github.com/stsquad/retry). 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?


Answer: 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.

