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

1

7

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?

Dickinson answered 4/3, 2013 at 11:40 Comment(0)
A
11

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.

Ambala answered 6/3, 2013 at 20:48 Comment(7)
I gave that a try and it doesn't have any effect: >retry.py -v -- ~/mysrc/retry.git/tty_tests command is ['/home/ajb/mysrc/retry.git/tty_tests'] isatty reports 1 pgrps are 28268 and -1 child finished: rc = 0 Ran command 1 timesDickinson
OH! I just noticed that you gave a link to retry.py in your question. I had thought that was just stackoverflow trying to be helpful and making a link out of something that looked like a hostname. I'll take a look.Ambala
Both os.tcsetgrp and os.setpgrp are returning None so it looks like it has failed :-/Dickinson
Is it a requirement that retry.py provide a tty to the subprocess, even when it's not run under a tty itself?Ambala
no although I'm not sure in what cases retry.py would be called. It's generally something I have running in the terminal.Dickinson
Bravo! Excellent edit and clarification thanks. It's always good to make the code simpler :-)Dickinson
become_tty_fg function can be a bit improved by adding os.close(tty) to its end. Otherwise file descriptor tty remains open and although in current code it is not important the function may be used in other places.Foeticide

© 2022 - 2024 — McMap. All rights reserved.