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

ajw*_*ood 34 python signals

我有一个命令,我正在包装script并使用Python脚本生成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
Run Code Online (Sandbox Code Playgroud)

我想按Ctrl-C退出python脚本.发生了什么,子进程死了,脚本继续.

udo*_*rog 24

默认情况下,子进程是同一进程组的一部分,只有一个可以控制和接收来自终端的信号,因此有几种不同的解决方案.

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

subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE)
Run Code Online (Sandbox Code Playgroud)

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

def preexec_function():
    os.setpgrp()

subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=preexec_function)
Run Code Online (Sandbox Code Playgroud)

明确忽略子进程中的信号

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

subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, preexec_fn=preexec_function)
Run Code Online (Sandbox Code Playgroud)

但是,这可能会被子进程覆盖.


Mat*_*son 9

拳头的事; 对象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...
Run Code Online (Sandbox Code Playgroud)

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

这是你的代码示例的一个riff,我认为你做了你想要的.信号处理程序只是将父进程(在本例中为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
Run Code Online (Sandbox Code Playgroud)

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

% python /tmp/proctest.py 
subproc returncode 0
subproc returncode 0
^Csubproc returncode -2
Run Code Online (Sandbox Code Playgroud)


ajw*_*ood 1

这个黑客可以工作,但它很难看......

将命令更改为:

success_flag = '/tmp/success.flag'
cmd = [ 'script', '-q', '-c', "sleep 2 && touch " + success_flag, '/dev/null']
Run Code Online (Sandbox Code Playgroud)

并把

if os.path.isfile( success_flag ) :
    os.remove( success_flag )
else :
    return
Run Code Online (Sandbox Code Playgroud)

在 for 循环的末尾