266 lines
8.3 KiB
Python
266 lines
8.3 KiB
Python
# -*- coding: utf-8 -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
|
|
|
|
"""Des fonctions pour lancer et gérer des processus externes
|
|
"""
|
|
|
|
__all__ = ('is_trace', 'is_stop_on_errors', 'spawn', 'spawnall', 'spawnone',
|
|
'LinePumper', 'DataPumper', 'spawn_capture', 'spawn_redirect')
|
|
|
|
import os, sys, re
|
|
from select import select
|
|
from threading import Thread
|
|
import subprocess
|
|
|
|
from .base import isstr
|
|
from .uio import _s
|
|
from .control import Status, OK_STATUS
|
|
|
|
def __spawnlp(mode, cmd, *args):
|
|
return subprocess.call([cmd] + list(args))
|
|
|
|
def __spawn_capture(capture_out, copy_err_on_out, capture_err, cmd, *args):
|
|
cmd = (cmd,) + args
|
|
stdout = subprocess.PIPE
|
|
if capture_out and copy_err_on_out: stderr = subprocess.STDOUT
|
|
else: stderr = subprocess.PIPE
|
|
proc = subprocess.Popen(cmd, stdout=stdout, stderr=stderr, close_fds=True)
|
|
stdout, stderr = proc.communicate()
|
|
if not capture_out: stdout = None
|
|
if not capture_err: stderr = None
|
|
return Status(exitcode=proc.returncode), stdout, stderr
|
|
|
|
def __spawn_redirect(inf, outf, errf, copy_errf_on_outf, cmd, *args):
|
|
cmd = (cmd,) + args
|
|
if copy_errf_on_outf: errf = subprocess.STDOUT
|
|
proc = subprocess.Popen(cmd, stdin=inf, stdout=outf, stderr=errf)
|
|
proc.communicate()
|
|
return proc.returncode
|
|
|
|
TRACE = 'trace'
|
|
STOP_ON_ERRORS = 'stop_on_errors'
|
|
STDIN = 'stdin'
|
|
STDOUT = 'stdout'
|
|
STDERR = 'stderr'
|
|
|
|
def is_trace(kw):
|
|
"""Indiquer si spawn doit afficher le nom de la commande qui est lancée.
|
|
"""
|
|
return kw.get(TRACE, False)
|
|
def is_stop_on_errors(kw):
|
|
"""Indiquer si spawnall doit s'arrêter en cas d'erreur.
|
|
"""
|
|
return kw.get(STOP_ON_ERRORS, True)
|
|
|
|
RE_ESCAPE = re.compile(r"\s|'")
|
|
def __quote(s):
|
|
if RE_ESCAPE.search(s) is not None:
|
|
s = s.replace("'", "'\''")
|
|
return "'%s'" % s
|
|
return s
|
|
|
|
def spawn(cmd, *args, **kw):
|
|
"""Lancer la commande cmd avec les arguments args, et retourner son status
|
|
d'exécution. La commande est cherchée dans le PATH si nécessaire.
|
|
|
|
@rtype: Status
|
|
@raise OSError: Si la commande n'est pas trouvée dans le PATH.
|
|
"""
|
|
if is_trace(kw):
|
|
cmdline = "$ %s" % cmd
|
|
for arg in args:
|
|
cmdline += " %s" % __quote(arg)
|
|
print cmdline
|
|
exitcode = __spawnlp(os.P_WAIT, cmd, *args)
|
|
if exitcode == 127:
|
|
raise OSError(exitcode, "Command not found: %s" % _s(cmd))
|
|
return Status(exitcode)
|
|
|
|
def spawnall(*argss, **kw):
|
|
"""Lancer toutes les commandes de argss, en s'arrêtant à la première erreur.
|
|
|
|
Si kw[STOP_ON_ERRORS] == False, on lance toutes les commande sans se
|
|
préoccuper de leur code de retour.
|
|
Retourner le status d'exécution de la dernière commande lancée.
|
|
|
|
@rtype: Status
|
|
"""
|
|
stop_on_errors = is_stop_on_errors(kw)
|
|
status = OK_STATUS
|
|
for args in argss:
|
|
status = spawn(*args, **kw)
|
|
if not status and stop_on_errors: break
|
|
return status
|
|
|
|
def spawnone(*argss, **kw):
|
|
"""Lancer toutes les commandes de argss, en s'arrêtant à la première
|
|
commande qui s'exécute sans erreur.
|
|
|
|
@rtype: Status
|
|
"""
|
|
status = OK_STATUS
|
|
for args in argss:
|
|
status = spawn(*args, **kw)
|
|
if status: break
|
|
return status
|
|
|
|
class Pumper(Thread):
|
|
"""Un thread qui lit les données sur un flux et l'écrit sur un autre.
|
|
|
|
En principe, outf est un objet fichier.
|
|
Si outf est None, alors les méthodes write_data, flush_outf et close_outf
|
|
doivent être surchargées pour implémenter la nouvelle stratégie d'écriture.
|
|
"""
|
|
def __init__(self, inf, outf,
|
|
stop_on_eof=True, close_on_eof=True,
|
|
flush_on_write=True,
|
|
start=True):
|
|
Thread.__init__(self)
|
|
self.setDaemon(True)
|
|
self.pumping = True
|
|
self.inf = inf
|
|
self.outf = outf
|
|
self.stop_on_eof = stop_on_eof
|
|
self.close_on_eof = close_on_eof
|
|
self.flush_on_write = flush_on_write
|
|
if start: self.start()
|
|
|
|
def read_data(self):
|
|
raise NotImplementedError
|
|
def write_data(self, data):
|
|
self.outf.write(data)
|
|
def flush_outf(self):
|
|
self.outf.flush()
|
|
def close_outf(self):
|
|
self.outf.close()
|
|
|
|
def run(self):
|
|
while self.pumping:
|
|
ins = [self.inf]
|
|
if self.outf is None: outs = []
|
|
else: [self.outf]
|
|
inr, outr, _ = select(ins, outs, [], 0.1)
|
|
if self.inf in inr and (not outs or self.outf in outr):
|
|
data = self.read_data()
|
|
if data:
|
|
self.write_data(data)
|
|
if self.flush_on_write: self.flush_outf()
|
|
elif self.stop_on_eof:
|
|
if self.close_on_eof: self.close_outf()
|
|
break
|
|
|
|
def stop(self):
|
|
"""Arrêter le pompage dès que possible.
|
|
"""
|
|
self.pumping = False
|
|
self.join()
|
|
|
|
class LinePumper(Pumper):
|
|
"""Un pumper qui lit les données ligne par ligne
|
|
"""
|
|
def read_data(self):
|
|
return self.inf.readline()
|
|
|
|
class DataPumper(Pumper):
|
|
"""Un pumper qui lit les données par quantité de bufsize.
|
|
"""
|
|
bufsize = 4096
|
|
|
|
def set_bufsize(self, bufsize):
|
|
self.bufsize = bufsize
|
|
|
|
def read_data(self):
|
|
return self.inf.read(self.bufsize)
|
|
|
|
class StringPumperMixin:
|
|
"""Un mixin à utiliser avec les classes dérivées de Pumper pour écrire
|
|
dans une chaine plutôt que sur un flux.
|
|
"""
|
|
buffer = None
|
|
|
|
def write_data(self, data):
|
|
buffer = self.buffer
|
|
if buffer is None: buffer = ""
|
|
buffer += data
|
|
self.buffer = buffer
|
|
def flush_outf(self): pass
|
|
def close_outf(self): pass
|
|
|
|
class StringLinePumper(StringPumperMixin, LinePumper):
|
|
pass
|
|
|
|
class StringDataPumper(StringPumperMixin, DataPumper):
|
|
pass
|
|
|
|
def spawn_capture(cmd, *args, **kw):
|
|
"""Lancer la commande cmd avec les arguments args, et retourner son status
|
|
d'exécution, ainsi que sa sortie standard et (éventuellement) sa sortie
|
|
d'erreur sous forme de chaine.
|
|
|
|
Si kw ne contient pas la clé stdout avec une valeur fausse, la sortie
|
|
standard sera capturée. Si kw contient la clé stderr avec une valeur vraie,
|
|
la sortie d'erreur sera capturée. Si kw contient la clé stderr avec une
|
|
valeur fausse, la sortie d'erreur ne sera pas capturée. Sinon, la sortie
|
|
d'erreur sera connectée sur la sortie standard.
|
|
|
|
@rtype: tuple
|
|
@return: (status, stdout, stderr)
|
|
"""
|
|
capture_out = kw.get(STDOUT, True) and True or False
|
|
capture_err = kw.get(STDERR, None)
|
|
if capture_err is None:
|
|
copy_err_on_out = True
|
|
else:
|
|
copy_err_on_out = False
|
|
capture_err = capture_err and True or False
|
|
return __spawn_capture(capture_out, copy_err_on_out, capture_err, cmd, *args)
|
|
|
|
def spawn_redirect(cmd, *args, **kw):
|
|
"""Lancer la commande cmd avec les arguments args, et retourner son status
|
|
d'exécution.
|
|
|
|
Si kw contient la clé stdin, l'entrée standard de cmd sera connectée sur
|
|
ce fichier. Si kw contient la clé stdout, la sortie standard de cmd sera
|
|
connectée sur ce fichier. Si kw ne contient pas la clé stderr, la sortie
|
|
d'erreur de cmd sera connectée sur stdout si défini.
|
|
|
|
Les valeurs stdout et stderr qui sont retournées sont les valeurs prises
|
|
de kw telles quelles.
|
|
|
|
@rtype: tuple
|
|
@return: (status, stdout, stderr)
|
|
"""
|
|
inf = stdin = kw.get(STDIN, None); close_inf = False
|
|
outf = stdout = kw.get(STDOUT, None); close_outf = False
|
|
errf = stderr = kw.get(STDERR, None); close_errf = False
|
|
if stdin is None and stdout is None and stderr is None:
|
|
return spawn(cmd, *args, **kw), None, None
|
|
|
|
if inf is None:
|
|
inf = sys.stdin
|
|
elif isstr(inf):
|
|
inf = open(inf, 'rb')
|
|
close_inf = True
|
|
if outf is None:
|
|
outf = sys.stdout
|
|
elif isstr(outf):
|
|
outf = open(outf, 'wb')
|
|
close_outf = True
|
|
copy_errf_on_outf = errf is None
|
|
if isstr(errf):
|
|
errf = open(errf, 'wb')
|
|
close_errf = True
|
|
|
|
if is_trace(kw):
|
|
cmdline = "$ %s" % cmd
|
|
for arg in args:
|
|
cmdline += " %s" % __quote(arg)
|
|
print cmdline
|
|
|
|
try:
|
|
exitcode = __spawn_redirect(inf, outf, errf, copy_errf_on_outf, cmd, *args)
|
|
return Status(exitcode=exitcode), stdout, stderr
|
|
finally:
|
|
if close_errf: errf.close()
|
|
if close_outf: outf.close()
|
|
if close_inf: inf.close()
|