web-dev-qa-db-fra.com

S'assurer qu'un script Python avec des sous-processus meurt sur SIGINT

J'ai une commande que j'emballe dans script et que je génère à partir d'un script Python à l'aide de subprocess.Popen. J'essaie de m'assurer qu'il mourra si l'utilisateur émet une SIGINT.

Je pourrais savoir si le processus a été interrompu d'au moins deux manières:

A. Die si la commande encapsulée a un statut de sortie différent de zéro (ne fonctionne pas, car script semble toujours renvoyer 0)

B. Faites quelque chose de spécial avec SIGINT dans le script Python parent plutôt que d’interrompre simplement le sous-processus. J'ai essayé ce qui suit:

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

J'aimerais appuyer sur Ctrl-C pour quitter le script python. Au lieu de cela, le sous-processus meurt et le script continue.

31
ajwood

Ce bidouillage va marcher, mais c'est moche ...

Changez la commande en ceci:

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

Et met

if os.path.isfile( success_flag ) :
    os.remove( success_flag )
else :
    return

à la fin de la boucle for

2
ajwood

Le sous-processus fait par défaut partie du même groupe de processus, et un seul peut contrôler et recevoir les signaux du terminal. Il existe donc deux solutions différentes.

Définir stdin en tant que PIPE (contrairement à l'héritage du processus parent), cela empêchera le processus enfant de recevoir les signaux qui lui sont associés.

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

Détachement du groupe de processus parent , l'enfant ne recevra plus de signaux

def preexec_function():
    os.setpgrp()

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

Ignorer explicitement les signaux dans le processus enfant

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

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

Cela peut toutefois être écrasé par le processus enfant.

24
udoprog

Chose de poing il existe une méthode send_signal() sur l'objet Popen. Si vous souhaitez envoyer un signal à celui que vous avez lancé, utilisez cette méthode pour l’envoyer.

Deuxième chose; un problème plus profond avec la façon dont vous configurez la communication avec votre sous-processus et ensuite, euh, ne communiquez pas avec lui. Vous ne pouvez pas indiquer en toute sécurité au sous-processus d'envoyer sa sortie à subprocess.PIPE et de ne pas le lire à partir des canaux. Les tubes UNIX sont mis en mémoire tampon (généralement une mémoire tampon 4K?) Et si le sous-processus remplit la mémoire tampon et que le processus à l'autre extrémité du canal ne lit pas les données mises en mémoire tampon, le sous-processus est suspendu ) lors de sa prochaine écriture sur le tuyau. Donc, le modèle habituel en utilisant subprocess.PIPE est d'appeler communicate() sur l'objet Popen.

Il n'est pas obligatoire d'utiliser subprocess.PIPE si vous souhaitez récupérer les données du sous-processus. Une astuce intéressante consiste à utiliser la classe tempfile.TemporaryFile pour créer un fichier temporaire non nommé (en réalité, il ouvre un fichier et supprime immédiatement l'inode du système de fichiers. Vous avez donc accès au fichier, mais personne d'autre ne peut l'ouvrir. Vous pouvez le faire. quelque chose comme:

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

Ensuite, vous pouvez lire le contenu de votre fichier temporaire (recherchez-le au début avant de le faire, pour vous assurer que vous êtes bien au début) lorsque vous savez que le sous-processus est terminé au lieu d'utiliser des canaux. Le problème de mise en mémoire tampon des canaux ne sera pas un problème si la sortie du sous-processus est transférée dans un fichier (temporaire ou non).

Voici un riff sur votre exemple de code qui, je pense, fait ce que vous voulez. Le gestionnaire de signaux répète simplement les signaux capturés par le processus parent (dans cet exemple, SIGINT et SIGTERM) à tous les sous-processus en cours (il ne doit en exister qu'un seul dans ce programme) et définit un indicateur au niveau du module indiquant de s'arrêter à la prochaine opportunité. Puisque j'utilise subprocess.PIPE I/O, j'appelle communicate() sur l'objet 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

Et en l'appelant (avec un Ctrl-C dans le troisième intervalle de 2 secondes):

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

Si vous n'avez pas de traitement python à effectuer après sa création (comme dans votre exemple), le moyen le plus simple consiste à utiliser os.execvp au lieu du module de sous-processus. Votre sous-processus va complètement remplacer votre processus python et sera celui qui intercepte directement SIGINT.

1
Scout

J'ai trouvé un commutateur -e dans la page de manuel du script:

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

Pas trop sûr de ce que le 128 + n est tout au sujet, mais il semble retourner 130 pour ctrl-c. Donc, en modifiant votre cmd pour être

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

et mettre

if p.returncode == 130:
    break

à la fin de la boucle semble faire ce que vous voulez.

0
Kevin Corcoran