2010-08-26 5 views
10

Quiero ejecutar un proceso, limitar el tiempo de ejecución por un tiempo de espera en segundos y capturar la salida producida por el proceso. Y quiero hacer esto en windows, linux y freebsd.subproceso python con tiempo de espera y salida grande (> 64K)

He tratado de implementar esto en tres formas diferentes:

  1. cmd - Sin tiempo de espera y subprocess.PIPE para la captura de salida.

    COMPORTAMIENTO: Funciona como se esperaba pero no admite tiempo de espera, necesito tiempo de espera ...

  2. cmd_to - Con tiempo de espera y subprocess.PIPE para la captura de salida.

    COMPORTAMIENTO: Bloquea la ejecución del subproceso cuando sale> = 2^16 bytes.

  3. cmd_totf - Con timeout y tempfile.NamedTemporaryfile para la captura de salida.

    COMPORTAMIENTO: Funciona como se esperaba pero utiliza archivos temporales en el disco.

Están disponibles a continuación para una inspección más detallada.

Como se puede ver en el resultado a continuación, el código de tiempo de espera bloquea la ejecución del subproceso cuando se utiliza el subproceso. PIPE y la salida del subproceso es> = 2^16 bytes.

La documentación del subproceso indica que esto se espera al llamar a process.wait() y al utilizar subprocessing.PIPE; sin embargo, no se dan advertencias al usar process.poll(), entonces, ¿qué está mal aquí?

Tengo una solución en cmd_totf que utiliza el módulo de tempfile pero la compensación es que escribe la salida en el disco, algo que REALMENTE quisiera evitar.

Así que mis preguntas son:

  • ¿qué estoy haciendo mal en cmd_to?
  • Hay una manera de hacer lo que quiero y sin usar archivos temporales/mantener la salida en la memoria.

script para generar un montón de salida ('exp_gen.py'):

#!/usr/bin/env python 
import sys 
output = "b"*int(sys.argv[1]) 
print output 

tres implementaciones diferentes (cmd cmd_to, cmd_totf) de envolturas alrededor subprocessing.Popen:

#!/usr/bin/env python 
import subprocess, time, tempfile 
bufsize = -1 

def cmd(cmdline, timeout=60): 
    """ 
    Execute cmdline. 
    Uses subprocessing and subprocess.PIPE. 
    """ 

    p = subprocess.Popen(
    cmdline, 
    bufsize = bufsize, 
    shell = False, 
    stdin = subprocess.PIPE, 
    stdout = subprocess.PIPE, 
    stderr = subprocess.PIPE 
) 

    out, err = p.communicate() 
    returncode = p.returncode 

    return (returncode, err, out) 

def cmd_to(cmdline, timeout=60): 
    """ 
    Execute cmdline, limit execution time to 'timeout' seconds. 
    Uses subprocessing and subprocess.PIPE. 
    """ 

    p = subprocess.Popen(
    cmdline, 
    bufsize = bufsize, 
    shell = False, 
    stdin = subprocess.PIPE, 
    stdout = subprocess.PIPE, 
    stderr = subprocess.PIPE 
) 

    t_begin   = time.time()    # Monitor execution time 
    seconds_passed = 0 

    while p.poll() is None and seconds_passed < timeout: 
    seconds_passed = time.time() - t_begin 
    time.sleep(0.1) 

    #if seconds_passed > timeout: 
    # 
    # try: 
    # p.stdout.close() # If they are not closed the fds will hang around until 
    # p.stderr.close() # os.fdlimit is exceeded and cause a nasty exception 
    # p.terminate()  # Important to close the fds prior to terminating the process! 
    #      # NOTE: Are there any other "non-freed" resources? 
    # except: 
    # pass 
    # 
    # raise TimeoutInterrupt 

    out, err = p.communicate() 
    returncode = p.returncode 

    return (returncode, err, out) 

def cmd_totf(cmdline, timeout=60): 
    """ 
    Execute cmdline, limit execution time to 'timeout' seconds. 
    Uses subprocessing and tempfile instead of subprocessing.PIPE. 
    """ 

    output = tempfile.NamedTemporaryFile(delete=False) 
    error = tempfile.NamedTemporaryFile(delete=False) 

    p = subprocess.Popen(
    cmdline, 
    bufsize = 0, 
    shell = False, 
    stdin = None, 
    stdout = output, 
    stderr = error 
) 

    t_begin   = time.time()    # Monitor execution time 
    seconds_passed = 0 

    while p.poll() is None and seconds_passed < timeout: 
    seconds_passed = time.time() - t_begin 
    time.sleep(0.1) 

    #if seconds_passed > timeout: 
    # 
    # try: 
    # p.stdout.close() # If they are not closed the fds will hang around until 
    # p.stderr.close() # os.fdlimit is exceeded and cause a nasty exception 
    # p.terminate()  # Important to close the fds prior to terminating the process! 
    #      # NOTE: Are there any other "non-freed" resources? 
    # except: 
    # pass 
    # 
    # raise TimeoutInterrupt 

    p.wait() 

    returncode = p.returncode 

    fd   = open(output.name) 
    out   = fd.read() 
    fd.close() 

    fd = open(error.name) 
    err = fd.read() 
    fd.close() 

    error.close() 
    output.close() 

    return (returncode, err, out) 

if __name__ == "__main__": 

    implementations = [cmd, cmd_to, cmd_totf] 
    bytes  = ['65535', '65536', str(1024*1024)] 
    timeouts = [5] 

    for timeout in timeouts:  
    for size in bytes:  
     for i in implementations: 
     t_begin   = time.time() 
     seconds_passed = 0   
     rc, err, output = i(['exp_gen.py', size], timeout) 
     seconds_passed = time.time() - t_begin 
     filler = ' '*(8-len(i.func_name)) 
     print "[%s%s: timeout=%d, iosize=%s, seconds=%f]" % (repr(i.func_name), filler, timeout, size, seconds_passed) 

salida de la ejecución:

['cmd'  : timeout=5, iosize=65535, seconds=0.016447] 
['cmd_to' : timeout=5, iosize=65535, seconds=0.103022] 
['cmd_totf': timeout=5, iosize=65535, seconds=0.107176] 
['cmd'  : timeout=5, iosize=65536, seconds=0.028105] 
['cmd_to' : timeout=5, iosize=65536, seconds=5.116658] 
['cmd_totf': timeout=5, iosize=65536, seconds=0.104905] 
['cmd'  : timeout=5, iosize=1048576, seconds=0.025964] 
['cmd_to' : timeout=5, iosize=1048576, seconds=5.128062] 
['cmd_totf': timeout=5, iosize=1048576, seconds=0.103183] 
+0

Pruebe la respuesta de http://stackoverflow.com/questions/874815/how-do-i-get-real-time-information-back-from-a-subprocess-popen-in-python-2-5 . –

+0

Debe mencionar qué versión de python. Debido a que AFAIK, hubo bastantes cambios de 2.6 a 2.7 relacionados con el módulo 'subprocess' –

+0

Vea también http://stackoverflow.com/questions/1191374/subprocess-with-timeout/8507775#8507775 – bortzmeyer

Respuesta

4

a diferencia de todos las advertencias en la documentación del subproceso, luego la lectura directa de process.stdout y process.stderr ha proporcionado una mejor solución.

Mejor me refiero a que puedo leer la salida de un proceso que excede 2^16 bytes sin tener que almacenar temporalmente la salida en el disco.

El código sigue:

import fcntl 
import os 
import subprocess 
import time 

def nonBlockRead(output): 
    fd = output.fileno() 
    fl = fcntl.fcntl(fd, fcntl.F_GETFL) 
    fcntl.fcntl(fd, fcntl.F_SETFL, fl | os.O_NONBLOCK) 
    try: 
     return output.read() 
    except: 
     return '' 

def cmd(cmdline, timeout=60): 
    """ 
    Execute cmdline, limit execution time to 'timeout' seconds. 
    Uses the subprocess module and subprocess.PIPE. 

    Raises TimeoutInterrupt 
    """ 

    p = subprocess.Popen(
     cmdline, 
     bufsize = bufsize, # default value of 0 (unbuffered) is best 
     shell = False, # not really needed; it's disabled by default 
     stdout = subprocess.PIPE, 
     stderr = subprocess.PIPE 
    ) 

    t_begin = time.time() # Monitor execution time 
    seconds_passed = 0 

    stdout = '' 
    stderr = '' 

    while p.poll() is None and seconds_passed < timeout: # Monitor process 
     time.sleep(0.1) # Wait a little 
     seconds_passed = time.time() - t_begin 

     # p.std* blocks on read(), which messes up the timeout timer. 
     # To fix this, we use a nonblocking read() 
     # Note: Not sure if this is Windows compatible 
     stdout += nonBlockRead(p.stdout) 
     stderr += nonBlockRead(p.stderr) 

    if seconds_passed >= timeout: 
     try: 
      p.stdout.close() # If they are not closed the fds will hang around until 
      p.stderr.close() # os.fdlimit is exceeded and cause a nasty exception 
      p.terminate()  # Important to close the fds prior to terminating the process! 
           # NOTE: Are there any other "non-freed" resources? 
     except: 
      pass 

     raise TimeoutInterrupt 

    returncode = p.returncode 

    return (returncode, stdout, stderr) 
+0

Esto es bueno, pero bloquea en lectura si no hay salida para leer, lo que daña el temporizador. Lo arreglé en mi versión y le agregué una edición. –

+0

@JohnDoe: ['fcntl' no funciona en Windows] (http://stackoverflow.com/q/375427/4279) – jfs

1

de responsabilidad: Esta respuesta no se ha probado en las ventanas, ni FreeBSD. Pero los módulos usados ​​deberían funcionar en estos sistemas. Creo que esta debería ser una respuesta operativa a tu pregunta: me funciona.

Aquí está el código que acabo de hackear para resolver el problema en Linux. Es una combinación de varios hilos de Stackoverflow y mi propia investigación en los documentos de Python 3.

características principales de este código:

  • no utiliza procesos de hilos para el bloqueo de E/S, ya que con mayor fiabilidad pueden p.terminated()
  • implementa un organismo de control de tiempo de espera reactivable que reinicia contando cada vez que parte de la producción
  • sucede
  • implementa un organismo de control de tiempo de espera a largo plazo para limitar el tiempo de ejecución global
  • puede alimentar en la entrada estándar (aunque yo sólo necesito para alimentarse en una sola vez cadenas cortas)
  • Puede capturar stdout/stderr en los medios habituales de Popen (solo stdout está codificado y stderr redirigido a stdout; pero puede separarse fácilmente)
  • Es casi en tiempo real porque solo comprueba cada 0,2 segundos para la salida. Pero puede disminuir esto o eliminar fácilmente el intervalo de espera
  • Muchas de las impresiones de depuración siguen habilitadas para ver qué ocurre cuando.

La única dependencia de código es enum como se implementó here, pero el código podría cambiarse fácilmente para que funcione sin él. Solo se usa para distinguir los dos tiempos de espera; use excepciones por separado si lo desea.

Aquí está el código - como de costumbre - retroalimentación es muy apreciada: (Edición 29-Jun-2012 - el código está trabajando realmente)

# Python module runcmd 
# Implements a class to launch shell commands which 
# are killed after a timeout. Timeouts can be reset 
# after each line of output 
# 
# Use inside other script with: 
# 
# import runcmd 
# (return_code, out) = runcmd.RunCmd(['ls', '-l', '/etc'], 
#         timeout_runtime, 
#         timeout_no_output, 
#         stdin_string).go() 
# 

import multiprocessing 
import queue 
import subprocess 
import time 

import enum 

def timestamp(): 
    return time.strftime('%Y%m%d-%H%M%S') 


class ErrorRunCmd(Exception): pass 
class ErrorRunCmdTimeOut(ErrorRunCmd): pass 

class Enqueue_output(multiprocessing.Process): 
    def __init__(self, out, queue): 
     multiprocessing.Process.__init__(self) 
     self.out = out 
     self.queue = queue 
     self.daemon = True 
    def run(self): 
     try: 
      for line in iter(self.out.readline, b''): 
       #print('worker read:', line) 
       self.queue.put(line) 
     except ValueError: pass # Readline of closed file 
     self.out.close() 
class Enqueue_input(multiprocessing.Process): 
    def __init__(self, inp, iterable): 
     multiprocessing.Process.__init__(self) 
     self.inp = inp 
     self.iterable = iterable 
     self.daemon = True 
    def run(self): 
     #print("writing stdin") 
     for line in self.iterable: 
      self.inp.write(bytes(line,'utf-8')) 
     self.inp.close() 
     #print("writing stdin DONE") 

class RunCmd(): 
    """RunCmd - class to launch shell commands 

    Captures and returns stdout. Kills child after a given 
    amount (timeout_runtime) wallclock seconds. Can also 
    kill after timeout_retriggerable wallclock seconds. 
    This second timer is reset whenever the child does some 
    output 

     (return_code, out) = RunCmd(['ls', '-l', '/etc'], 
            timeout_runtime, 
            timeout_no_output, 
            stdin_string).go() 

    """ 
    Timeout = enum.Enum('No','Retriggerable','Runtime') 

    def __init__(self, cmd, timeout_runtime, timeout_retriggerable, stdin=None): 
     self.dbg = False 
     self.cmd = cmd 
     self.timeout_retriggerable = timeout_retriggerable 
     self.timeout_runtime = timeout_runtime 
     self.timeout_hit = self.Timeout.No 
     self.stdout = '--Cmd did not yield any output--' 
     self.stdin = stdin 
    def read_queue(self, q): 
     time_last_output = None 
     try: 
      bstr = q.get(False) # non-blocking 
      if self.dbg: print('{} chars read'.format(len(bstr))) 
      time_last_output = time.time() 
      self.stdout += bstr 
     except queue.Empty: 
      #print('queue empty') 
      pass 
     return time_last_output 
    def go(self): 
     if self.stdin: 
      pstdin = subprocess.PIPE 
     else: 
      pstdin = None 
     p = subprocess.Popen(self.cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=pstdin) 
     pin = None 
     if (pstdin): 
      pin = Enqueue_input(p.stdin, [self.stdin + '\n']) 
      pin.start() 
     q = multiprocessing.Queue() 
     pout = Enqueue_output(p.stdout, q) 
     pout.start() 
     try: 
      if self.dbg: print('Beginning subprocess with timeout {}/{} s on {}'.format(self.timeout_retriggerable, self.timeout_runtime, time.asctime())) 
      time_begin = time.time() 
      time_last_output = time_begin 
      seconds_passed = 0 
      self.stdout = b'' 
      once = True     # ensure loop's executed at least once 
             # some child cmds may exit very fast, but still produce output 
      while once or p.poll() is None or not q.empty(): 
       once = False 
       if self.dbg: print('a) {} of {}/{} secs passed and overall {} chars read'.format(seconds_passed, self.timeout_retriggerable, self.timeout_runtime, len(self.stdout))) 

       tlo = self.read_queue(q) 
       if tlo: 
        time_last_output = tlo 

       now = time.time() 
       if now - time_last_output >= self.timeout_retriggerable: 
        self.timeout_hit = self.Timeout.Retriggerable 
        raise ErrorRunCmdTimeOut(self) 
       if now - time_begin >= self.timeout_runtime: 
        self.timeout_hit = self.Timeout.Runtime 
        raise ErrorRunCmdTimeOut(self) 

       if q.empty(): 
        time.sleep(0.1) 
      # Final try to get "last-millisecond" output 
      self.read_queue(q)    
     finally: 
      self._close(p, [pout, pin])    
     return (self.returncode, self.stdout)    

    def _close(self, p, procs): 
     if self.dbg: 
      if self.timeout_hit != self.Timeout.No: 
       print('{} A TIMEOUT occured: {}'.format(timestamp(), self.timeout_hit)) 
      else: 
       print('{} No timeout occured'.format(timestamp())) 
     for process in [proc for proc in procs if proc]: 
      try: 
       process.terminate() 
      except: 
       print('{} Process termination raised trouble'.format(timestamp())) 
       raise 
     try: 
      p.stdin.close() 
     except: pass 
     if self.dbg: print('{} _closed stdin'.format(timestamp())) 
     try: 
      p.stdout.close() # If they are not closed the fds will hang around until 
     except: pass 
     if self.dbg: print('{} _closed stdout'.format(timestamp())) 
      #p.stderr.close() # os.fdlimit is exceeded and cause a nasty exception 
     try: 
      p.terminate()  # Important to close the fds prior to terminating the process! 
           # NOTE: Are there any other "non-freed" resources? 
     except: pass 
     if self.dbg: print('{} _closed Popen'.format(timestamp())) 
     try: 
      self.stdout = self.stdout.decode('utf-8') 
     except: pass 
     self.returncode = p.returncode 
     if self.dbg: print('{} _closed all'.format(timestamp())) 

uso con:

import runcmd 

cmd = ['ls', '-l', '/etc'] 

worker = runcmd.RunCmd(cmd, 
         40, # limit runtime [wallclock seconds] 
         2,  # limit runtime after last output [wallclk secs] 
         ''  # stdin input string 
         ) 
(return_code, out) = worker.go() 

if worker.timeout_hit != worker.Timeout.No: 
    print('A TIMEOUT occured: {}'.format(worker.timeout_hit)) 
else: 
    print('No timeout occured') 


print("Running '{:s}' returned {:d} and {:d} chars of output".format(cmd, return_code, len(out))) 
print('Output:') 
print(out) 

command - el primer argumento - debe ser una lista de un comando y sus argumentos. Se utiliza para la llamada Popen(shell=False) y sus tiempos de espera están en segundos. Actualmente no hay código para desactivar los tiempos de espera. Establezca timeout_no_output en time_runtime para desactivar efectivamente el timeout_no_output reajustable. stdin_string puede ser cualquier cadena que se envíe a la entrada estándar del comando. Establezca en None si su comando no necesita ninguna entrada. Si se proporciona una cadena, se agrega una '\ n' final.

Cuestiones relacionadas