2011-01-17 1 views
12

ho scritto un piccolo script Python che esegue alcuni comandi di shell utilizzando il modulo subprocess e una funzione di supporto:Decorare delegato un oggetto File per aggiungere funzionalità

import subprocess as sp 
def run(command, description): 
    """Runs a command in a formatted manner. Returns its return code.""" 
    start=datetime.datetime.now() 
    sys.stderr.write('%-65s' % description) 
    s=sp.Popen(command, shell=True, stderr=sp.PIPE, stdout=sp.PIPE) 
    out,err=s.communicate() 
    end=datetime.datetime.now() 
    duration=end-start 
    status='Done' if s.returncode==0 else 'Failed' 
    print '%s (%d seconds)' % (status, duration.seconds) 

Le seguenti righe si legge l'output standard e errore:

s=sp.Popen(command, shell=True, stderr=sp.PIPE, stdout=sp.PIPE) 
    out,err=s.communicate() 

Come si può vedere, stdout e stderr non vengono utilizzati. Supponiamo che voglio scrivere i messaggi di output e di errore in un file di log, in modo formattato, per es .:

[STDOUT: 2011-01-17 14:53:55] <message> 
[STDERR: 2011-01-17 14:53:56] <message> 

La mia domanda è, qual è il modo più Pythonic per farlo? Ho pensato a tre opzioni:

  1. Ereditare l'oggetto file e sovrascrivere il metodo write.
  2. Utilizzare una classe Delegate che implementa write.
  3. Connettersi allo PIPE stesso in qualche modo.

UPDATE: riferimento script di test

Sto controllando i risultati con questo script, salvato come test.py:

#!/usr/bin/python 
import sys 

sys.stdout.write('OUT\n') 
sys.stdout.flush() 
sys.stderr.write('ERR\n') 
sys.stderr.flush() 

Tutte le idee?

+3

mi arrendo. Sono stato in grado di avvolgere (sottoclasse) il file in modo che le scritture aggiungano timestamp e titolo del file, ma Popen lavora su fds e non riesco a vedere dove avvengono le scritture. Sembra che sia solo "os.execvp" e che l'handle dello stdout sia magico. Così ho sottoclassato Popen ma ignora il fatto che sono stati passati file compressi e una mappa di fd: title. – TryPyPy

risposta

12

1 e 2 sono soluzioni ragionevoli, ma sovrascrivere write() non sarà sufficiente.

Il problema è che Popen ha bisogno di handle di file da allegare al processo, quindi gli oggetti file Python non funzionano, devono essere a livello di SO. Per risolvere questo devi avere un oggetto Python che abbia un handle di file di livello os. L'unico modo che posso pensare di risolvere è usare le pipe, quindi hai un handle di file di livello OS su cui scrivere. Ma poi hai bisogno di un altro thread che si trovi e sondaggi quella pipa per le cose da leggere in modo che possa registrarlo. (Quindi questa è più strettamente un'implementazione di 2, in quanto delega alla registrazione).

Detto e fatto:

import io 
import logging 
import os 
import select 
import subprocess 
import time 
import threading 

LOG_FILENAME = 'output.log' 
logging.basicConfig(filename=LOG_FILENAME,level=logging.DEBUG) 

class StreamLogger(io.IOBase): 
    def __init__(self, level): 
     self.level = level 
     self.pipe = os.pipe() 
     self.thread = threading.Thread(target=self._flusher) 
     self.thread.start() 

    def _flusher(self): 
     self._run = True 
     buf = b'' 
     while self._run: 
      for fh in select.select([self.pipe[0]], [], [], 0)[0]: 
       buf += os.read(fh, 1024) 
       while b'\n' in buf: 
        data, buf = buf.split(b'\n', 1) 
        self.write(data.decode()) 
      time.sleep(1) 
     self._run = None 

    def write(self, data): 
     return logging.log(self.level, data) 

    def fileno(self): 
     return self.pipe[1] 

    def close(self): 
     if self._run: 
      self._run = False 
      while self._run is not None: 
       time.sleep(1) 
      os.close(self.pipe[0]) 
      os.close(self.pipe[1]) 

Così quella classe inizia un tubo livello di sistema operativo che Popen possibile allegare il/out/errore stdin per per il sottoprocesso. Inoltre, avvia una discussione che esegue il polling dell'altra estremità di quel pipe una volta al secondo per le operazioni da registrare, che quindi registra con il modulo di registrazione.

Forse questa classe dovrebbe attuare altre cose per completezza, ma funziona in questo caso comunque.

codice Esempio:

with StreamLogger(logging.INFO) as out: 
    with StreamLogger(logging.ERROR) as err: 
     subprocess.Popen("ls", stdout=out, stderr=err, shell=True) 

output.log finisce così:

INFO:root:output.log 
INFO:root:streamlogger.py 
INFO:root:and 
INFO:root:so 
INFO:root:on 

testato con Python 2.6, 2.7 e 3.1.

penserei qualsiasi implementazione di 1 e 3 avrebbe bisogno di utilizzare tecniche simili. È un po 'complicato, ma a meno che tu non possa creare correttamente il log dei comandi di Popen, non ho un'idea migliore).

+0

+1: questo è simile a quello che stavo per suggerire, ma non ho potuto ottenere il corretto funzionamento del thread di registrazione. – senderle

+1

funzionerà su Windows ?! ho dei dubbi, perché 'select' su win è solo per socket, non file. –

+0

Non l'ho provato su Windows, ma in caso contrario sostituirlo con qualunque cosa si faccia per verificare se c'è qualcosa da leggere su una pipe sotto Windows. –

1

Suggerisco l'opzione 3, con il pacchetto della libreria standard logging. In questo caso, direi che gli altri 2 erano eccessivi.

+3

Come reindirizzare il 'PIPE'? –

0

Questo utilizza Adam Rosenfield's make_async and read_async. Mentre la mia risposta originale era select.epoll ed era quindi solo per Linux, ora utilizza select.select, che dovrebbe funzionare sotto Unix o Windows.

Questo registra uscita dal sottoprocesso a /tmp/test.log come accade:

import logging 
import subprocess 
import shlex 
import select 
import fcntl 
import os 
import errno 

def make_async(fd): 
    # https://stackoverflow.com/a/7730201/190597 
    '''add the O_NONBLOCK flag to a file descriptor''' 
    fcntl.fcntl(fd, fcntl.F_SETFL, fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK) 

def read_async(fd): 
    # https://stackoverflow.com/a/7730201/190597 
    '''read some data from a file descriptor, ignoring EAGAIN errors''' 
    try: 
     return fd.read() 
    except IOError, e: 
     if e.errno != errno.EAGAIN: 
      raise e 
     else: 
      return '' 

def log_process(proc,stdout_logger,stderr_logger): 
    loggers = { proc.stdout: stdout_logger, proc.stderr: stderr_logger } 
    def log_fds(fds): 
     for fd in fds: 
      out = read_async(fd) 
      if out.strip(): 
       loggers[fd].info(out) 
    make_async(proc.stdout) 
    make_async(proc.stderr) 
    while True: 
     # Wait for data to become available 
     rlist, wlist, xlist = select.select([proc.stdout, proc.stderr], [], []) 
     log_fds(rlist) 
     if proc.poll() is not None: 
      # Corner case: check if more output was created 
      # between the last call to read_async and now 
      log_fds([proc.stdout, proc.stderr])     
      break 

if __name__=='__main__': 
    formatter = logging.Formatter('[%(name)s: %(asctime)s] %(message)s') 
    handler = logging.FileHandler('/tmp/test.log','w') 
    handler.setFormatter(formatter) 

    stdout_logger=logging.getLogger('STDOUT') 
    stdout_logger.setLevel(logging.DEBUG) 
    stdout_logger.addHandler(handler) 

    stderr_logger=logging.getLogger('STDERR') 
    stderr_logger.setLevel(logging.DEBUG) 
    stderr_logger.addHandler(handler)   

    proc = subprocess.Popen(shlex.split('ls -laR /tmp'), 
          stdout=subprocess.PIPE, 
          stderr=subprocess.PIPE) 
    log_process(proc,stdout_logger,stderr_logger) 
+1

E 'una bella soluzione, ma mantiene lo stdout \ stderr da parte fino al termine del processo, e poi li scrive in un file di log. Ciò significa che i timestamp indicare i tempi di registrazione, non è il momento degli eventi, e perde traccia dell'ordine degli eventi (tutti i messaggi stdout sono scritti, e quindi tutti i messaggi stderr sono scritti). –

0

1 e 2 non funzionerà. Ecco un'implementazione del principio:

import subprocess 
import time 

FileClass = open('tmptmp123123123.tmp', 'w').__class__ 

class WrappedFile(FileClass): 
    TIMETPL = "%Y-%m-%d %H:%M:%S" 
    TEMPLATE = "[%s: %s] " 

    def __init__(self, name, mode='r', buffering=None, title=None): 
     self.title = title or name 

     if buffering is None: 
      super(WrappedFile, self).__init__(name, mode) 
     else: 
      super(WrappedFile, self).__init__(name, mode, buffering) 

    def write(self, s): 
     stamp = time.strftime(self.TIMETPL) 
     if not s: 
      return 
     # Add a line with timestamp per line to be written 
     s = s.split('\n') 
     spre = self.TEMPLATE % (self.title, stamp) 
     s = "\n".join(["%s %s" % (spre, line) for line in s]) + "\n" 
     super(WrappedFile, self).write(s) 

Il motivo per cui non funziona è che non chiama mai Popen stdout.write. Un file spostato funzionerà correttamente quando chiameremo il suo metodo di scrittura e verrà anche scritto su Passen, ma la scrittura avverrà in un livello inferiore, saltando il metodo di scrittura.

0

Questa semplice soluzione ha funzionato per me:

import sys 
import datetime 
import tempfile 
import subprocess as sp 
def run(command, description): 
    """Runs a command in a formatted manner. Returns its return code.""" 
    with tempfile.SpooledTemporaryFile(8*1024) as so: 
     print >> sys.stderr, '%-65s' % description 
     start=datetime.datetime.now() 
     retcode = sp.call(command, shell=True, stderr=sp.STDOUT, stdout=so) 
     end=datetime.datetime.now() 
     so.seek(0) 
     for line in so.readlines(): 
      print >> sys.stderr,'logging this:', line.rstrip() 
     duration=end-start 
     status='Done' if retcode == 0 else 'Failed' 
     print >> sys.stderr, '%s (%d seconds)' % (status, duration.seconds) 

REF_SCRIPT = r"""#!/usr/bin/python 
import sys 

sys.stdout.write('OUT\n') 
sys.stdout.flush() 
sys.stderr.write('ERR\n') 
sys.stderr.flush() 
""" 

SCRIPT_NAME = 'refscript.py' 

if __name__ == '__main__': 
    with open(SCRIPT_NAME, 'w') as script: 
     script.write(REF_SCRIPT) 
    run('python ' + SCRIPT_NAME, 'Reference script') 
+0

Funziona con errori \ risultati di output misti? Ha fallito il mio script di riferimento (vedi aggiornamento sopra). –

+0

@Adam Matan L'ho provato con 'run ('ls/nonexistent', 'Does not exist')' per controllare 'stderr', e ha funzionato. È necessario assicurarsi che sia 'stderr = sp.SDOUT' nella chiamata a' Popen'. A proposito, sto cambiando il codice per rimuovere il ciclo 's.poll()' perché è probabilmente un ciclo non necessario, a carica rapida. – Apalala

+0

@Adam Matan. Funziona anche con il tuo script di riferimento. Vedi le modifiche. Sto usando Ubuntu 10.10 e Python 2.6.6). – Apalala