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