nutools/lib/nulib/python/nulib/procs.py

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()