45

I've got a command that I'm wrapping in script and spawning from a Python script using subprocess.Popen. I'm trying to make sure it dies if the user issues a SIGINT.

I could figure out if the process was interrupted in a least two ways:

A. Die if the wrapped command has a non-zero exit status (doesn't work, because script seems to always return 0)

B. Do something special with SIGINT in the parent Python script rather than simply interrupting the subprocess. I've tried the following:

import sys
import signal
import subprocess

def interrupt_handler(signum, frame):
    print "While there is a 'script' subprocess alive, this handler won't executes"
    sys.exit(1)
signal.signal(signal.SIGINT, interrupt_handler)

for n in range( 10 ):
    print "Going to sleep for 2 second...Ctrl-C to exit the sleep cycles"

    # exit 1 if we make it to the end of our sleep
    cmd = [ 'script', '-q', '-c', "sleep 2 && (exit 1)", '/dev/null']
    p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)

    while True:
        if p.poll() != None :
            break
        else :
            pass

    # Exiting on non-zero exit status would suffice
    print "Exit status (script always exits zero, despite what happened to the wrapped command):", p.returncode

I'd like hitting Ctrl-C to exit the python script. What's happening instead is the subprocess dies and the script continues.

ajwood
  • 18,227
  • 15
  • 61
  • 104
  • 3
    Thanks! I was a little discouraged by the close vote actually.. sometimes I accidentally commit a faux pas, and people jump all over it with nastiness rather than explaining why I shouldn't do what I'm doing :) – ajwood Nov 27 '12 at 21:43
  • Is this question similar to [this one](http://stackoverflow.com/questions/3791398)? If so, the answer doesn't seem to be easy. :( – myeeshen Dec 05 '12 at 17:44
  • Hmmm I'm afraid it might be.. handling the signal correctly would be my first choice, but it looks like I might have to settle for the -e switch Kevin found. – ajwood Dec 05 '12 at 18:41
  • How many processes do you have? Exactly which of them die at SIGINT? Isn't it the problem that script in the subprocess doesn't die when its child dies? – pts Dec 08 '12 at 15:38
  • I can have any number of subprocesses, but I spawn them off in series; I only spawn a new one after the previous one closes. On SIGINT, a subprocess that dies, and the next subprocess immediately starts up, rather than what I'd like, which is the whole thing to grind to a halt. – ajwood Dec 10 '12 at 14:33

5 Answers5

35

The subprocess is by default part of the same process group, and only one can control and receive signals from the terminal, so there are a couple of different solutions.

Setting stdin as a PIPE (in contrast to inheriting from the parent process), this will prevent the child process from receiving signals associated to it.

subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)

Detaching from the parent process group, the child will no longer receive signals

def preexec_function():
    os.setpgrp()

subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=preexec_function)

Explicitly ignoring signals in the child process

def preexec_function():
    signal.signal(signal.SIGINT, signal.SIG_IGN)

subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=preexec_function)

This might however be overwritten by the child process.

udoprog
  • 1,825
  • 14
  • 14
  • 1
    Unfortunately, none of these options for for me. 1) I need the child to inherit STDIN from the parents. 2) The (graphical) subprocess won't start. 3) Subprocess definitely ignores SIGINT, but reinstalling my master handler won't work either. – ajwood Dec 07 '12 at 14:06
  • if subprocess.check_call() is used I see all the processes in the subprocess hierarchy are receiving the signals from bottom-up sequence. This is contradictory to what @udoprog has mentioned "only one can control and receive signals from the terminal". Can some one clarify me? – merlachandra Mar 10 '20 at 09:36
14

Fist thing; there is a send_signal() method on the Popen object. If you want to send a signal to one you've launched, use this method to send it.

Second thing; a deeper problem with the way you're setting up communication with your subprocess and then, um, not communicating with it. You cannot safely tell the subprocess to send its output to subprocess.PIPE and then not read from the pipes. UNIX pipes are buffered (typically a 4K buffer?), and if the subprocess fills up the buffer and the process on the other end of the pipe doesn't read the buffered data, the subprocess will pend (locking up, from an observer's perspective) on its next write to the pipe. So, the usual pattern when using subprocess.PIPE is to call communicate() on the Popen object.

It is not mandatory to use subprocess.PIPE if you want data back from the subprocess. A cool trick is to use the tempfile.TemporaryFile class to make an unnamed temp file (really it opens a file and immediately deletes the inode from the file system, so you have access to the file but no-one else can open one. You can do something like:

with tempfile.TemporaryFile() as iofile:
    p = Popen(cmd, stdout=iofile, stderr=iofile)
    while True:
        if p.poll() is not None:
            break
        else:
            time.sleep(0.1) # without some sleep, this polling is VERY busy...

Then you can read the contents of your temporary file (seek to the beginning of it before you do, to be sure you're at the beginning) when you know the subprocess has exited, instead of using pipes. The pipe buffering problem won't be a problem if the subprocess's output is going to a file (temporary or not).

Here is a riff on your code sample that I think does what you want. The signal handler just repeats the signals being trapped by the parent process (in this example, SIGINT and SIGTERM) to all current subprocesses (there should only ever be one in this program) and sets a module-level flag saying to shutdown at the next opportunity. Since I'm using subprocess.PIPE I/O, I call communicate() on the Popen object.

#!/usr/bin/env python

from subprocess import Popen, PIPE
import signal
import sys

current_subprocs = set()
shutdown = False

def handle_signal(signum, frame):
    # send signal recieved to subprocesses
    global shutdown
    shutdown = True
    for proc in current_subprocs:
        if proc.poll() is None:
            proc.send_signal(signum)


signal.signal(signal.SIGINT, handle_signal)
signal.signal(signal.SIGTERM, handle_signal)

for _ in range(10):
    if shutdown:
        break
    cmd = ["sleep", "2"]
    p = Popen(cmd, stdout=PIPE, stderr=PIPE)
    current_subprocs.add(p)
    out, err = p.communicate()
    current_subprocs.remove(p)
    print "subproc returncode", p.returncode

And calling it (with a Ctrl-C in the third 2 second interval):

% python /tmp/proctest.py 
subproc returncode 0
subproc returncode 0
^Csubproc returncode -2
Matt Anderson
  • 19,311
  • 11
  • 41
  • 57
  • This one doesn't work for me. The command needs to be wrapped in ``script``, and if you change you example to ``cmd = ["script", "-q", "-c", "sleep 2", "/dev/null"]``, it no longer works. – ajwood Dec 10 '12 at 14:48
  • I in fact do read from the ``subprocess.PIPE``, but I boiled my code down to what I thought was minimal to make it suitable to put in an SO question. Should I expand it a bit more? – ajwood Dec 10 '12 at 14:53
  • FYI, I'm spawning a graphical process which needs command line interaction. It has a nasty habit of holding on to it's output when I spawn it from my Python script, so when it halts and expects me to input something, it doesn't actually tell me what it's waiting for. Wrapping it in a ``script`` fixed my in/out command line stuff, but introduced the signalling problems. – ajwood Dec 10 '12 at 14:59
  • @ajwood: what is "script", and how does it respond to a SIGINT signal? Is it a python script? Does it have a signal handler itself? If "script" is what you are launching, this pattern will send that signal to the underlying process when you hit ctrl-c; it is up to that process to respond appropriately, however. You could consider when receiving SIGINT to the master process, sending something more firm to "script", such as SIGTERM or SIGKILL (if necessary). – Matt Anderson Dec 11 '12 at 02:43
  • [script](http://en.wikipedia.org/wiki/Script_(Unix)) is a standard Linux utility. I don't know the specifics of how it handles SIGINT. I don't understand why, but when `script` is the subprocess, the signal handler in this pattern never executes. – ajwood Dec 11 '12 at 15:13
  • @ajwood: presumably, the signal handler does execute, relaying a SIGINT to the linux utility script, but the subprocess program traps and ignores the signal. You could, in the signal handler, instead of `proc.send_signal(signum)` do `proc.send_signal(signal.SIGTERM)`. That is a more forceful instruction to the underlying process to stop. And you could also put a `print` statement (or function for python3) in the signal handler to assure yourself that it executed. – Matt Anderson Dec 12 '12 at 15:13
  • I don't think I'm explaining the symptom well enough. There is no problem with getting the subprocess stopped; what I can't get to work is killing the subprocess *and* the master Python script on SIGINT. – ajwood Dec 12 '12 at 15:55
  • @ajwood: the example above is designed to wait until the subprocess has exited, and then itself exit. It's a common pattern for processes managing other processes to do something like send SIGINT, SIGTERM, and SIGKILL to a given subprocess that is supposed to exit, maybe 10 seconds apart (SIGKILL will do the job for sure). If you want the top-level process to exit immediately and trust the subprocess to exit on its own, eventually, send the signal of your choice to the subprocess and then call sys.exit(NUM) in your top-level script. – Matt Anderson Dec 13 '12 at 01:12
  • Do you have a Linux box you can try this on? Your example, as is, works perfectly. The trouble is that it does not work if the sleep process is spawned via `script`. Change `cmd` to what I proposed in my first comment, and `handle_signal` will not fire. – ajwood Dec 13 '12 at 13:46
  • See my edited print statement at the start of `interrupt_handler` – ajwood Dec 13 '12 at 13:51
1

This hack will work, but it's ugly...

Change the command to this:

success_flag = '/tmp/success.flag'
cmd = [ 'script', '-q', '-c', "sleep 2 && touch " + success_flag, '/dev/null']

And put

if os.path.isfile( success_flag ) :
    os.remove( success_flag )
else :
    return

at the end of the for loop

ajwood
  • 18,227
  • 15
  • 61
  • 104
1

If you have no python processing to do after your process is spawned (like in your example), then the easiest way is to use os.execvp instead of the subprocess module. Your subprocess is going to completely replace your python process, and will be the one catching SIGINT directly.

Scout
  • 550
  • 2
  • 5
  • I think this would only get me to the first or the ten subprocess that should run in this example, no? – ajwood Dec 10 '12 at 20:04
0

I found a -e switch in the script man page:

-e      Return the exit code of the child process. Uses the same format
         as bash termination on signal termination exit code is 128+n.

Not too sure what the 128+n is all about but it seems to return 130 for ctrl-c. So modifying your cmd to be

cmd = [ 'script', '-e', '-q', '-c', "sleep 2 && (exit 1)", '/dev/null']

and putting

if p.returncode == 130:
    break

at the end of the for loop seems to do what you want.