web-dev-qa-db-ja.com

Pythonサブプロセスを含むスクリプトがSIGINTで終了することを確認する

私はscriptでラップし、Pythonスクリプトを使用してsubprocess.Popen。ユーザーがSIGINTを発行した場合に確実に終了するようにしています。

プロセスが少なくとも2つの方法で中断されたかどうかを確認できました。

A.ラップされたコマンドの終了ステータスがゼロ以外の場合は死にます(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スクリプトを終了します。代わりにサブプロセスが停止し、スクリプトが続行します。

35
ajwood

このハックは機能しますが、醜いです...

コマンドを次のように変更します。

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ループの終わり

1
ajwood

サブプロセスは、デフォルトでは同じプロセスグループの一部であり、ターミナルからの信号を制御および受信できるのは1つだけなので、いくつかの異なるソリューションがあります。

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)

ただし、これは子プロセスによって上書きされる可能性があります。

24
udoprog

こぶし物; Popenオブジェクトにはsend_signal()メソッドがあります。起動したシグナルにシグナルを送信する場合は、このメソッドを使用してシグナルを送信します。

第二のもの;サブプロセスとの通信をセットアップする方法にさらに問題があり、サブプロセスと通信しないサブプロセスにその出力を_subprocess.PIPE_に送信して、パイプから読み取らないように安全に指示することはできません。 UNIXパイプはバッファリングされ(通常4Kバッファ?)、サブプロセスがバッファを満たし、パイプの反対側のプロセスがバッファリングされたデータを読み取らない場合、サブプロセスは保留されます(ロックアップ、オブザーバーの観点から)。 )パイプへの次の書き込み時。したがって、_subprocess.PIPE_を使用するときの通常のパターンは、Popenオブジェクトでcommunicate()を呼び出すことです。

サブプロセスからデータを戻す場合は、_subprocess.PIPE_を使用する必要はありません。クールなトリックは、_tempfile.TemporaryFile_クラスを使用して名前のない一時ファイルを作成することです(実際にファイルを開き、ファイルシステムからiノードをすぐに削除するため、ファイルにアクセスできますが、他の誰もファイルを開くことはできません) 。次のようなことができます。

_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)によってトラップされているシグナルを現在のすべてのサブプロセス(このプログラムには1つだけ存在する必要があります)に繰り返し、次のシャットダウンを示すモジュールレベルのフラグを設定します機会。私は_subprocess.PIPE_ I/Oを使用しているので、Popenオブジェクトでcommunicate()を呼び出します。

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

そしてそれを呼び出す(3番目の2秒間隔で_Ctrl-C_を使用):

_% python /tmp/proctest.py 
subproc returncode 0
subproc returncode 0
^Csubproc returncode -2
_
10
Matt Anderson

(例のように)プロセスが生成された後に行うpython処理がない場合は、サブプロセスモジュールの代わりにos.execvpを使用するのが最も簡単な方法です。サブプロセスはpythonプロセスを完全に置き換え、SIGINTを直接キャッチするプロセスになります。

1
Scout

スクリプトのマニュアルページに-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ループの最後で、あなたが望むことをしているようです。

0