41

我有一个命令,我script使用subprocess.Popen. 如果用户发出SIGINT.

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

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

B.SIGINT在父 Python 脚本中做一些特别的事情,而不是简单地中断子进程。我尝试了以下方法:

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 脚本。相反,发生的是子进程死亡并且脚本继续。

4

5 回答 5

32

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

将 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)

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

于 2012-12-06T05:23:30.243 回答
12

拳头东西;对象上有一个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...

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

这是您的代码示例的即兴演奏,我认为它可以满足您的需求。信号处理程序只是将父进程捕获的信号(在本例中为 SIGINT 和 SIGTERM)重复到所有当前子进程(该程序中应该只有一个),并设置一个模块级标志,表示在下一次关闭机会。因为我使用的是subprocess.PIPEI/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

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

% python /tmp/proctest.py 
subproc returncode 0
subproc returncode 0
^Csubproc returncode -2
于 2012-12-09T16:26:52.277 回答
1

这个hack会起作用,但它很难看......

将命令更改为:

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 循环结束时

于 2012-12-05T21:38:55.207 回答
1

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

于 2012-12-08T15:29:05.303 回答
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 循环结束时似乎做你想做的事。

于 2012-12-04T20:36:46.563 回答