我有一个命令,我正在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脚本。发生的事情是子进程死了,脚本继续。
答案 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循环结束时的似乎可以做你想要的。