Voici le code Python permettant d'exécuter une commande arbitraire renvoyant ses données stdout
ou de déclencher une exception sur des codes de sortie non nuls:
proc = subprocess.Popen(
cmd,
stderr=subprocess.STDOUT, # Merge stdout and stderr
stdout=subprocess.PIPE,
Shell=True)
communicate
est utilisé pour attendre la fin du processus:
stdoutdata, stderrdata = proc.communicate()
Le module subprocess
ne prend pas en charge le délai d'attente - possibilité de tuer un processus exécuté pendant plus de X secondes - par conséquent, communicate
peut prendre une éternité.
Quel est le moyen le plus simple d'implémenter les délais dans un programme Python destiné à s'exécuter sous Windows et Linux?
En Python 3.3+:
from subprocess import STDOUT, check_output
output = check_output(cmd, stderr=STDOUT, timeout=seconds)
output
est une chaîne d'octets contenant les données stderr, fusionnées et fusionnées de la commande.
Ce code déclenche CalledProcessError
sur un statut de sortie autre que zéro, comme spécifié dans le texte de la question, contrairement à la méthode proc.communicate()
.
J'ai supprimé Shell=True
car il est souvent utilisé inutilement. Vous pouvez toujours le rajouter si cmd
l'exige effectivement. Si vous ajoutez Shell=True
, c’est-à-dire si le processus enfant génère ses propres descendants; check_output()
peut revenir beaucoup plus tard que le délai imparti, voir Échec du délai d'attente du sous-processus .
La fonctionnalité de délai d'attente est disponible sur Python 2.x via le répertoire subprocess32
du module de sous-processus 3.2+.
Je ne connais pas grand chose aux détails de bas niveau. mais, étant donné que dans python 2.6, l’API offre la possibilité d’attendre les threads et de terminer les processus .__, qu’en est-il de l’exécuter dans un thread séparé
import subprocess, threading
class Command(object):
def __init__(self, cmd):
self.cmd = cmd
self.process = None
def run(self, timeout):
def target():
print 'Thread started'
self.process = subprocess.Popen(self.cmd, Shell=True)
self.process.communicate()
print 'Thread finished'
thread = threading.Thread(target=target)
thread.start()
thread.join(timeout)
if thread.is_alive():
print 'Terminating process'
self.process.terminate()
thread.join()
print self.process.returncode
command = Command("echo 'Process started'; sleep 2; echo 'Process finished'")
command.run(timeout=3)
command.run(timeout=1)
La sortie de cet extrait de code sur ma machine est la suivante:
Thread started
Process started
Process finished
Thread finished
0
Thread started
Process started
Terminating process
Thread finished
-15
où on peut voir que, dans la première exécution, le processus s'est terminé correctement (code retour 0), tandis que dans la seconde, le processus a été terminé (code retour -15).
Je n'ai pas testé sous Windows; mais, en dehors de la mise à jour de l'exemple de commande , je pense que cela devrait fonctionner car je n'ai trouvé dans la documentation rien qui indique que thread.join ou process.terminate n'est pas pris en charge.
la réponse de jcollado peut être simplifiée à l’aide du threading.Timer class:
import shlex
from subprocess import Popen, PIPE
from threading import Timer
def run(cmd, timeout_sec):
proc = Popen(shlex.split(cmd), stdout=PIPE, stderr=PIPE)
timer = Timer(timeout_sec, proc.kill)
try:
timer.start()
stdout, stderr = proc.communicate()
finally:
timer.cancel()
# Examples: both take 1 second
run("sleep 1", 5) # process ends normally at 1 second
run("sleep 5", 1) # timeout happens at 1 second
Si vous êtes sur Unix,
import signal
...
class Alarm(Exception):
pass
def alarm_handler(signum, frame):
raise Alarm
signal.signal(signal.SIGALRM, alarm_handler)
signal.alarm(5*60) # 5 minutes
try:
stdoutdata, stderrdata = proc.communicate()
signal.alarm(0) # reset the alarm
except Alarm:
print "Oops, taking too long!"
# whatever else
Voici la solution d'Alex Martelli en tant que module permettant de tuer correctement les processus. Les autres approches ne fonctionnent pas car elles n'utilisent pas proc.communicate (). Donc, si vous avez un processus qui produit beaucoup de sortie, il remplira son tampon de sortie puis se bloquera jusqu'à ce que vous lisiez quelque chose dessus.
from os import kill
from signal import alarm, signal, SIGALRM, SIGKILL
from subprocess import PIPE, Popen
def run(args, cwd = None, Shell = False, kill_tree = True, timeout = -1, env = None):
'''
Run a command with a timeout after which it will be forcibly
killed.
'''
class Alarm(Exception):
pass
def alarm_handler(signum, frame):
raise Alarm
p = Popen(args, Shell = Shell, cwd = cwd, stdout = PIPE, stderr = PIPE, env = env)
if timeout != -1:
signal(SIGALRM, alarm_handler)
alarm(timeout)
try:
stdout, stderr = p.communicate()
if timeout != -1:
alarm(0)
except Alarm:
pids = [p.pid]
if kill_tree:
pids.extend(get_process_children(p.pid))
for pid in pids:
# process might have died before getting to this line
# so wrap to avoid OSError: no such process
try:
kill(pid, SIGKILL)
except OSError:
pass
return -9, '', ''
return p.returncode, stdout, stderr
def get_process_children(pid):
p = Popen('ps --no-headers -o pid --ppid %d' % pid, Shell = True,
stdout = PIPE, stderr = PIPE)
stdout, stderr = p.communicate()
return [int(p) for p in stdout.split()]
if __== '__main__':
print run('find /', Shell = True, timeout = 3)
print run('find', Shell = True)
J'ai modifié sussudio answer. La fonction retourne maintenant: (returncode
, stdout
, stderr
, timeout
) - stdout
et stderr
est décodé en chaîne utf-8
def kill_proc(proc, timeout):
timeout["value"] = True
proc.kill()
def run(cmd, timeout_sec):
proc = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
timeout = {"value": False}
timer = Timer(timeout_sec, kill_proc, [proc, timeout])
timer.start()
stdout, stderr = proc.communicate()
timer.cancel()
return proc.returncode, stdout.decode("utf-8"), stderr.decode("utf-8"), timeout["value"]
personne surpris a mentionné l'utilisation de timeout
timeout 5 ping -c 3 somehost
Cela ne fonctionnera évidemment pas pour tous les cas d'utilisation, mais si vous utilisez un script simple, il est difficile à battre.
Egalement disponible en tant que gtimeout dans coreutils via homebrew
pour les utilisateurs mac.
Une autre option consiste à écrire dans un fichier temporaire pour empêcher le blocage de stdout au lieu de devoir interroger avec communic (). Cela a fonctionné pour moi là où les autres réponses n’ont pas fonctionné; par exemple sur windows.
outFile = tempfile.SpooledTemporaryFile()
errFile = tempfile.SpooledTemporaryFile()
proc = subprocess.Popen(args, stderr=errFile, stdout=outFile, universal_newlines=False)
wait_remaining_sec = timeout
while proc.poll() is None and wait_remaining_sec > 0:
time.sleep(1)
wait_remaining_sec -= 1
if wait_remaining_sec <= 0:
killProc(proc.pid)
raise ProcessIncompleteError(proc, timeout)
# read temp streams from start
outFile.seek(0);
errFile.seek(0);
out = outFile.read()
err = errFile.read()
outFile.close()
errFile.close()
timeout
est maintenant supporté par call()
et communicate()
dans le module de sous-processus (à partir de Python3.3):
import subprocess
subprocess.call("command", timeout=20, Shell=True)
Cela va appeler la commande et lever l'exception
subprocess.TimeoutExpired
si la commande ne se termine pas après 20 secondes.
Vous pouvez ensuite gérer l'exception pour continuer votre code, quelque chose comme:
try:
subprocess.call("command", timeout=20, Shell=True)
except subprocess.TimeoutExpired:
# insert code here
J'espère que cela t'aides.
Voici ma solution, j'utilisais Thread and Event:
import subprocess
from threading import Thread, Event
def kill_on_timeout(done, timeout, proc):
if not done.wait(timeout):
proc.kill()
def exec_command(command, timeout):
done = Event()
proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
watcher = Thread(target=kill_on_timeout, args=(done, timeout, proc))
watcher.daemon = True
watcher.start()
data, stderr = proc.communicate()
done.set()
return data, stderr, proc.returncode
En action:
In [2]: exec_command(['sleep', '10'], 5)
Out[2]: ('', '', -9)
In [3]: exec_command(['sleep', '10'], 11)
Out[3]: ('', '', 0)
La solution que j'utilise consiste à préfixer la commande Shell avec timelimit . Si la commande prend trop de temps, timelimit l'arrêtera et Popen aura un code de retour défini par timelimit. Si elle est> 128, cela signifie que le délai imparti a tué le processus.
Voir aussi sous-processus python avec timeout et sortie importante (> 64 Ko)
J'ai ajouté la solution avec thread de jcollado
à mon module Python easyprocess .
Installer:
pip install easyprocess
Exemple:
from easyprocess import Proc
# Shell is not supported!
stdout=Proc('ping localhost').call(timeout=1.5).stdout
print stdout
si vous utilisez python 2, essayez-le
import subprocess32
try:
output = subprocess32.check_output(command, Shell=True, timeout=3)
except subprocess32.TimeoutExpired as e:
print e
Je ne sais pas pourquoi cela n'est pas mentionné, mais depuis Python 3.5, il existe une nouvelle commande subprocess.run
universal (destinée à remplacer check_call
, check_output
...) et qui comporte également le paramètre timeout
.
subprocess.run (arguments, *, stdin = Aucun, entrée = Aucun, stdout = Aucun, stderr = Aucun, Shell = Faux, cwd = Aucun, délai d'attente = Aucun, cocher = Faux, codage = Aucun, erreurs = Aucun)
Run the command described by args. Wait for command to complete, then return a CompletedProcess instance.
Il déclenche une exception subprocess.TimeoutExpired
à l'expiration du délai d'attente.
Une fois que vous avez compris le fonctionnement complet des machines dans * unix, vous trouverez facilement une solution plus simple:
Prenons cet exemple simple, qui explique comment rendre methode de communication () methode timeoutable en utilisant select.select () (disponible presque partout sur * nix de nos jours). Cela peut aussi être écrit avec epoll/poll/kqueue, mais la variante select.select () pourrait être un bon exemple pour vous. Et les principales limitations de select.select () (vitesse et 1024 FDS max) ne sont pas applicables pour votre tâche.
Cela fonctionne sous * nix, ne crée pas de threads, n’utilise pas de signaux, peut être lancé depuis n’importe quel thread (pas seulement principal) et rapide pour lire 250 Mo/s de données depuis stdout sur ma machine (i5 2.3ghz).
Il y a un problème pour joindre stdout/stderr à la fin de la communication. Si vous avez une sortie de programme énorme, cela pourrait entraîner une utilisation importante de la mémoire. Mais vous pouvez appeler plusieurs fois plusieurs fois avec plusieurs temps morts.
class Popen(subprocess.Popen):
def communicate(self, input=None, timeout=None):
if timeout is None:
return subprocess.Popen.communicate(self, input)
if self.stdin:
# Flush stdio buffer, this might block if user
# has been writing to .stdin in an uncontrolled
# fashion.
self.stdin.flush()
if not input:
self.stdin.close()
read_set, write_set = [], []
stdout = stderr = None
if self.stdin and input:
write_set.append(self.stdin)
if self.stdout:
read_set.append(self.stdout)
stdout = []
if self.stderr:
read_set.append(self.stderr)
stderr = []
input_offset = 0
deadline = time.time() + timeout
while read_set or write_set:
try:
rlist, wlist, xlist = select.select(read_set, write_set, [], max(0, deadline - time.time()))
except select.error as ex:
if ex.args[0] == errno.EINTR:
continue
raise
if not (rlist or wlist):
# Just break if timeout
# Since we do not close stdout/stderr/stdin, we can call
# communicate() several times reading data by smaller pieces.
break
if self.stdin in wlist:
chunk = input[input_offset:input_offset + subprocess._PIPE_BUF]
try:
bytes_written = os.write(self.stdin.fileno(), chunk)
except OSError as ex:
if ex.errno == errno.EPIPE:
self.stdin.close()
write_set.remove(self.stdin)
else:
raise
else:
input_offset += bytes_written
if input_offset >= len(input):
self.stdin.close()
write_set.remove(self.stdin)
# Read stdout / stderr by 1024 bytes
for fn, tgt in (
(self.stdout, stdout),
(self.stderr, stderr),
):
if fn in rlist:
data = os.read(fn.fileno(), 1024)
if data == '':
fn.close()
read_set.remove(fn)
tgt.append(data)
if stdout is not None:
stdout = ''.join(stdout)
if stderr is not None:
stderr = ''.join(stderr)
return (stdout, stderr)
Prévendre la commande Linux timeout
n’est pas une solution de contournement mauvaise et cela a fonctionné pour moi.
cmd = "timeout 20 "+ cmd
subprocess.Popen(cmd.split(), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
(output, err) = p.communicate()
Vous pouvez le faire en utilisant select
import subprocess
from datetime import datetime
from select import select
def call_with_timeout(cmd, timeout):
started = datetime.now()
sp = subprocess.Popen(cmd, stdout=subprocess.PIPE)
while True:
p = select([sp.stdout], [], [], timeout)
if p[0]:
p[0][0].read()
ret = sp.poll()
if ret is not None:
return ret
if (datetime.now()-started).total_seconds() > timeout:
sp.kill()
return None
J'ai mis en œuvre ce que je pourrais rassembler de quelques-uns d'entre eux. Cela fonctionne sous Windows, et comme il s'agit d'un wiki de communauté, je pense que je partagerais aussi mon code:
class Command(threading.Thread):
def __init__(self, cmd, outFile, errFile, timeout):
threading.Thread.__init__(self)
self.cmd = cmd
self.process = None
self.outFile = outFile
self.errFile = errFile
self.timed_out = False
self.timeout = timeout
def run(self):
self.process = subprocess.Popen(self.cmd, stdout = self.outFile, \
stderr = self.errFile)
while (self.process.poll() is None and self.timeout > 0):
time.sleep(1)
self.timeout -= 1
if not self.timeout > 0:
self.process.terminate()
self.timed_out = True
else:
self.timed_out = False
Puis d'une autre classe ou d'un autre fichier:
outFile = tempfile.SpooledTemporaryFile()
errFile = tempfile.SpooledTemporaryFile()
executor = command.Command(c, outFile, errFile, timeout)
executor.daemon = True
executor.start()
executor.join()
if executor.timed_out:
out = 'timed out'
else:
outFile.seek(0)
errFile.seek(0)
out = outFile.read()
err = errFile.read()
outFile.close()
errFile.close()
Bien que je n'y aie pas beaucoup réfléchi, ce décorateur que j'ai trouvé chez ActiveState semble très utile pour ce genre de chose. Avec subprocess.Popen(..., close_fds=True)
, au moins, je suis prêt pour le script shell en Python.
J'ai eu le problème que je voulais mettre fin à un sous-processus multithreading s'il prenait plus de temps qu'un délai donné. Je voulais définir un délai d'attente dans Popen()
, mais cela n'a pas fonctionné. Ensuite, j'ai réalisé que Popen().wait()
est égal à call()
et j'ai donc eu l'idée de définir un délai d'attente dans la méthode .wait(timeout=xxx)
, qui a finalement fonctionné. Ainsi, je l'ai résolu de cette façon:
import os
import sys
import signal
import subprocess
from multiprocessing import Pool
cores_for_parallelization = 4
timeout_time = 15 # seconds
def main():
jobs = [...YOUR_JOB_LIST...]
with Pool(cores_for_parallelization) as p:
p.map(run_parallel_jobs, jobs)
def run_parallel_jobs(args):
# Define the arguments including the paths
initial_terminal_command = 'C:\\Python34\\python.exe' # Python executable
function_to_start = 'C:\\temp\\xyz.py' # The multithreading script
final_list = [initial_terminal_command, function_to_start]
final_list.extend(args)
# Start the subprocess and determine the process PID
subp = subprocess.Popen(final_list) # starts the process
pid = subp.pid
# Wait until the return code returns from the function by considering the timeout.
# If not, terminate the process.
try:
returncode = subp.wait(timeout=timeout_time) # should be zero if accomplished
except subprocess.TimeoutExpired:
# Distinguish between Linux and Windows and terminate the process if
# the timeout has been expired
if sys.platform == 'linux2':
os.kill(pid, signal.SIGTERM)
Elif sys.platform == 'win32':
subp.terminate()
if __== '__main__':
main()
Il y a une idée pour sous-classer la classe Popen et l'étendre avec quelques décorateurs de méthodes simples. Appelons cela ExpirablePopen.
from logging import error
from subprocess import Popen
from threading import Event
from threading import Thread
class ExpirablePopen(Popen):
def __init__(self, *args, **kwargs):
self.timeout = kwargs.pop('timeout', 0)
self.timer = None
self.done = Event()
Popen.__init__(self, *args, **kwargs)
def __tkill(self):
timeout = self.timeout
if not self.done.wait(timeout):
error('Terminating process {} by timeout of {} secs.'.format(self.pid, timeout))
self.kill()
def expirable(func):
def wrapper(self, *args, **kwargs):
# zero timeout means call of parent method
if self.timeout == 0:
return func(self, *args, **kwargs)
# if timer is None, need to start it
if self.timer is None:
self.timer = thr = Thread(target=self.__tkill)
thr.daemon = True
thr.start()
result = func(self, *args, **kwargs)
self.done.set()
return result
return wrapper
wait = expirable(Popen.wait)
communicate = expirable(Popen.communicate)
if __== '__main__':
from subprocess import PIPE
print ExpirablePopen('ssh -T [email protected]', stdout=PIPE, timeout=1).communicate()
J'ai utilisé killableprocess avec succès sous Windows, Linux et Mac. Si vous utilisez Cygwin Python, vous aurez besoin de la version du processus killable de OSAF car, sinon, les processus Windows natifs ne seront pas tués.
Cette solution supprime l’arbre de processus si Shell = True, transmet des paramètres au processus (ou non), a un délai d’expiration et récupère les sorties stdout, stderr et process du rappel (elle utilise psutil pour kill_proc_tree). Cela reposait sur plusieurs solutions publiées dans SO, notamment jcollado. Publication en réponse aux commentaires d'Anson et de jradice dans la réponse de jcollado. Testé sous Windows Srvr 2012 et Ubuntu 14.04. Veuillez noter que pour Ubuntu, vous devez changer l'appel parent.children (...) en parent.get_children (...).
def kill_proc_tree(pid, including_parent=True):
parent = psutil.Process(pid)
children = parent.children(recursive=True)
for child in children:
child.kill()
psutil.wait_procs(children, timeout=5)
if including_parent:
parent.kill()
parent.wait(5)
def run_with_timeout(cmd, current_dir, cmd_parms, timeout):
def target():
process = subprocess.Popen(cmd, cwd=current_dir, Shell=True, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
# wait for the process to terminate
if (cmd_parms == ""):
out, err = process.communicate()
else:
out, err = process.communicate(cmd_parms)
errcode = process.returncode
thread = Thread(target=target)
thread.start()
thread.join(timeout)
if thread.is_alive():
me = os.getpid()
kill_proc_tree(me, including_parent=False)
thread.join()
pour Python 2.6+, utilisez gevent
from gevent.subprocess import Popen, PIPE, STDOUT
def call_sys(cmd, timeout):
p= Popen(cmd, Shell=True, stdout=PIPE)
output, _ = p.communicate(timeout=timeout)
assert p.returncode == 0, p. returncode
return output
call_sys('./t.sh', 2)
# t.sh example
sleep 5
echo done
exit 1
https://pypi.python.org/pypi/python-subprocess2 fournit des extensions du module de sous-processus qui vous permettent d’attendre jusqu’à un certain délai, sinon mettre fin.
Donc, attendre 10 secondes que le processus se termine, sinon, tuez:
pipe = subprocess.Popen('...')
timeout = 10
results = pipe.waitOrTerminate(timeout)
Ceci est compatible avec Windows et Unix. "résultats" est un dictionnaire, il contient "returnCode" qui est le retour de l'application (ou None si elle devait être tuée), ainsi que "actionTaken". qui sera "SUBPROCESS2_PROCESS_COMPLETED" si le processus s'est déroulé normalement, ou un masque de "SUBPROCESS2_PROCESS_TERMINATED" et de SUBPROCESS2_PROCESS_KILLED en fonction de l'action entreprise (voir la documentation pour plus de détails)
Malheureusement, je suis lié par des règles très strictes concernant la divulgation du code source par mon employeur. Je ne peux donc pas fournir de code. Mais à mon goût, la meilleure solution consiste à créer une sous-classe remplaçant Popen.wait()
pour interroger au lieu d'attendre indéfiniment, et Popen.__init__
pour accepter un paramètre de délai d'attente. Une fois que vous avez fait cela, toutes les autres méthodes Popen
(qui appellent wait
) fonctionneront comme prévu, y compris communicate
.
python 2.7
import time
import subprocess
def run_command(cmd, timeout=0):
start_time = time.time()
df = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
while timeout and df.poll() == None:
if time.time()-start_time >= timeout:
df.kill()
return -1, ""
output = '\n'.join(df.communicate()).strip()
return df.returncode, output