确保带有子进程的Python脚本在SIGINT上消失

时间:2012-11-27 21:17:04

标签: python signals

我有一个命令,我正在script包装,并使用subprocess.Popen从Python脚本中生成。我试图确保在用户发出SIGINT时它会死亡。

我可以通过至少两种方式判断该过程是否被中断:

一个。如果包装的命令具有非零退出状态则死亡(不起作用,因为script似乎总是返回0)

B中。在父Python脚本中使用SIGINT执行一些特殊操作,而不是简单地中断子流程。我尝试了以下内容:

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

我想按Ctrl-C退出python脚本。发生的事情是子进程死了,脚本继续。

5 个答案:

答案 0 :(得分:23)

子流程默认属于同一个流程组,只有一个可以控制和接收来自终端的信号,因此有几种不同的解决方案。

将stdin设置为PIPE (与继承父进程相反),这将阻止子进程接收与其关联的信号。

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

从父进程组中分离,子进程将不再接收信号

def preexec_function():
    os.setpgrp()

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

明确忽略子进程中的信号

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

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

然而,这可能会被子进程覆盖。

答案 1 :(得分:8)

拳头; send_signal()对象上有Popen方法。如果要向已启动的信号发送信号,请使用此方法发送信号。

第二件事;你设置与子进程的通信方式的一个更深层次的问题,然后,嗯,不与它通信。您无法安全地告诉子进程将其输出发送到subprocess.PIPE,然后不从管道中读取。 UNIX管道是缓冲的(通常是4K缓冲区?),如果子进程填满缓冲区并且管道另一端的进程没有读取缓冲数据,则子进程将挂起(从观察者的角度看,锁定) )在下一次写入管道时。因此,使用subprocess.PIPE时的常用模式是在communicate()对象上调用Popen

如果您希望从子进程返回数据,则不必使用subprocess.PIPE。一个很酷的技巧是使用tempfile.TemporaryFile类来创建一个未命名的临时文件(实际上它会打开一个文件并立即从文件系统中删除inode,因此您可以访问该文件,但是没有其他人可以打开一个文件你可以这样做:

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

然后,当您知道子进程已退出而不是使用管道时,您可以读取临时文件的内容(在开始之前查找它的开头,以确保您在开头)。如果子进程的输出转到文件(临时或非临时),则管道缓冲问题不会成为问题。

这是你的代码示例的一个riff,我认为你做了你想要的。信号处理程序只是将父进程(在本例中为SIGINT和SIGTERM)捕获的信号重复到所有当前子进程(此程序中应该只有一个)并设置一个模块级标志,表示在下一个进程中关闭机会。由于我正在使用subprocess.PIPE I / O,因此我会在communicate()对象上调用Popen

#!/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

并调用它(在第二个2秒间隔内使用Ctrl-C):

% python /tmp/proctest.py 
subproc returncode 0
subproc returncode 0
^Csubproc returncode -2

答案 2 :(得分:2)

这个黑客会起作用,但它太丑了......

将命令更改为:

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

然后把

if os.path.isfile( success_flag ) :
    os.remove( success_flag )
else :
    return
在for循环结束时

答案 3 :(得分:1)

如果在生成进程后没有进行python处理(如在您的示例中),那么最简单的方法是使用os.execvp而不是子进程模块。你的子进程将完全取代你的python进程,并且将直接捕获SIGINT。

答案 4 :(得分:0)

我在脚本手册页中找到了一个-e开关:

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

不太确定128 + n到底是什么,但它似乎为ctrl-c返回130。所以修改你的cmd

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

并且放

if p.returncode == 130:
    break
在for循环结束时的

似乎可以做你想要的。