2013-08-26 11 views
11

Una libreria interna che utilizza in gran parte il sottoprocesso.Popen() ha iniziato a non eseguire i test automatici quando è stato aggiornato da Python 2.7.3 a Python 2.7.5. Questa libreria è utilizzata in un ambiente con thread. Dopo aver eseguito il debug del problema, sono riuscito a creare un breve script Python che dimostra l'errore visto nei test non riusciti.Popen del sottoprocesso chiude gli script di stderr/stderr utilizzati in un'altra discussione quando si verificano errori di popen

Questo è lo script (chiamato "threadedsubprocess.py"):

import time 
import threading 
import subprocess 

def subprocesscall(): 
    p = subprocess.Popen(
     ['ls', '-l'], 
     stdin=subprocess.PIPE, 
     stdout=subprocess.PIPE, 
     stderr=subprocess.PIPE, 
     ) 
    time.sleep(2) # simulate the Popen call takes some time to complete. 
    out, err = p.communicate() 
    print 'succeeding command in thread:', threading.current_thread().ident 

def failingsubprocesscall(): 
    try: 
     p = subprocess.Popen(
      ['thiscommandsurelydoesnotexist'], 
      stdin=subprocess.PIPE, 
      stdout=subprocess.PIPE, 
      stderr=subprocess.PIPE, 
      ) 
    except Exception as e: 
     print 'failing command:', e, 'in thread:', threading.current_thread().ident 

print 'main thread is:', threading.current_thread().ident 

subprocesscall_thread = threading.Thread(target=subprocesscall) 
subprocesscall_thread.start() 
failingsubprocesscall() 
subprocesscall_thread.join() 

Nota: questo script non esce con un IOError quando correva da Python 2.7.3. Fallisce almeno il 50% delle volte quando viene eseguito da Python 2.7.5 (entrambi sulla stessa VM Ubuntu 12.04 64 bit).

L'errore che viene generato su Python 2.7.5 è questo:

/opt/python/2.7.5/bin/python ./threadedsubprocess.py 
main thread is: 139899583563520 
failing command: [Errno 2] No such file or directory 139899583563520 
Exception in thread Thread-1: 
Traceback (most recent call last): 
    File "/opt/python/2.7.5/lib/python2.7/threading.py", line 808, in __bootstrap_inner 
    self.run() 
    File "/opt/python/2.7.5/lib/python2.7/threading.py", line 761, in run 
    self.__target(*self.__args, **self.__kwargs) 
    File "./threadedsubprocess.py", line 13, in subprocesscall 
    out, err = p.communicate() 
    File "/opt/python/2.7.5/lib/python2.7/subprocess.py", line 806, in communicate 
    return self._communicate(input) 
    File "/opt/python/2.7.5/lib/python2.7/subprocess.py", line 1379, in _communicate 
    self.stdin.close() 
IOError: [Errno 9] Bad file descriptor 

close failed in file object destructor: 
IOError: [Errno 9] Bad file descriptor 

Se si confronta il modulo sottoprocesso da Python 2.7.3 a Python 2.7.5 vedo l'Popen() 's __init __() chiama infatti ora chiude esplicitamente i descrittori di file stdin, stdout e stderr nel caso in cui l'esecuzione del comando non riesca in qualche modo. Questa sembra essere una correzione intenzionale applicata in Python 2.7.4 per prevenire la perdita dei descrittori di file (http://hg.python.org/cpython/file/ab05e7dd2788/Misc/NEWS#l629).

Il diff tra Python 2.7.3 e Python 2.7.5 che sembra essere rilevante per questo problema è nel Popen __init __():

@@ -671,12 +702,33 @@ 
      c2pread, c2pwrite, 
      errread, errwrite) = self._get_handles(stdin, stdout, stderr) 

-  self._execute_child(args, executable, preexec_fn, close_fds, 
-       cwd, env, universal_newlines, 
-       startupinfo, creationflags, shell, 
-       p2cread, p2cwrite, 
-       c2pread, c2pwrite, 
-       errread, errwrite) 
+  try: 
+   self._execute_child(args, executable, preexec_fn, close_fds, 
+        cwd, env, universal_newlines, 
+        startupinfo, creationflags, shell, 
+        p2cread, p2cwrite, 
+        c2pread, c2pwrite, 
+        errread, errwrite) 
+  except Exception: 
+   # Preserve original exception in case os.close raises. 
+   exc_type, exc_value, exc_trace = sys.exc_info() 
+ 
+   to_close = [] 
+   # Only close the pipes we created. 
+   if stdin == PIPE: 
+    to_close.extend((p2cread, p2cwrite)) 
+   if stdout == PIPE: 
+    to_close.extend((c2pread, c2pwrite)) 
+   if stderr == PIPE: 
+    to_close.extend((errread, errwrite)) 
+ 
+   for fd in to_close: 
+    try: 
+     os.close(fd) 
+    except EnvironmentError: 
+     pass 
+ 
+   raise exc_type, exc_value, exc_trace 

penso di avere tre domande:

1) È vero che dovrebbe essere possibile utilizzare principalmente il sottoprocesso.Popen, con PIPE per stdin, stdout e stderr, in un ambiente con thread?

2) Come impedire che i descrittori di file per stdin, stdout e stderr vengano chiusi quando il Popen() non riesce in uno dei thread?

3) Sto facendo qualcosa di sbagliato qui?

+0

cosa interessante è che questo è stato risolto in Python 3.4+ – Cukic0d

risposta

7

vorrei rispondere alle vostre domande con:

  1. Sì.
  2. Non dovresti.
  3. No.

L'errore si verifica effettivamente in Python 2.7.4 pure.

Penso che questo sia un bug nel codice della libreria. Se aggiungi un blocco nel tuo programma e assicurati che le due chiamate a subprocess.Popen siano eseguite atomicamente, l'errore non si verifica.

@@ -1,32 +1,40 @@ 
import time 
import threading 
import subprocess 

+lock = threading.Lock() 
+ 
def subprocesscall(): 
+ lock.acquire() 
    p = subprocess.Popen(
     ['ls', '-l'], 
     stdin=subprocess.PIPE, 
     stdout=subprocess.PIPE, 
     stderr=subprocess.PIPE, 
     ) 
+ lock.release() 
    time.sleep(2) # simulate the Popen call takes some time to complete. 
    out, err = p.communicate() 
    print 'succeeding command in thread:', threading.current_thread().ident 

def failingsubprocesscall(): 
    try: 
+  lock.acquire() 
     p = subprocess.Popen(
      ['thiscommandsurelydoesnotexist'], 
      stdin=subprocess.PIPE, 
      stdout=subprocess.PIPE, 
      stderr=subprocess.PIPE, 
      ) 
    except Exception as e: 
     print 'failing command:', e, 'in thread:', threading.current_thread().ident 
+ finally: 
+  lock.release() 
+ 

print 'main thread is:', threading.current_thread().ident 

subprocesscall_thread = threading.Thread(target=subprocesscall) 
subprocesscall_thread.start() 
failingsubprocesscall() 
subprocesscall_thread.join() 

Ciò significa che si tratta molto probabilmente a causa di qualche gara di dati per l'attuazione del Popen. Io rischiare una supposizione: il bug può essere per l'attuazione del pipe_cloexec, chiamato da _get_handles, che (in 2.7.4) è:

def pipe_cloexec(self): 
    """Create a pipe with FDs set CLOEXEC.""" 
    # Pipes' FDs are set CLOEXEC by default because we don't want them 
    # to be inherited by other subprocesses: the CLOEXEC flag is removed 
    # from the child's FDs by _dup2(), between fork() and exec(). 
    # This is not atomic: we would need the pipe2() syscall for that. 
    r, w = os.pipe() 
    self._set_cloexec_flag(r) 
    self._set_cloexec_flag(w) 
    return r, w 

e il commento avverte esplicitamente di non essere atomica ... Questo sicuramente causa una corsa di dati ma, senza sperimentazione, non so se è ciò che causa il problema.

+0

Grazie. Se la tua valutazione è effettivamente corretta, ciò significherebbe che avrei archiviato un bug report per Python 2.7.5 e spero che in qualche modo otteniamo un altro bugfixrelease. Pensi che la mia domanda iniziale sarebbe abbastanza informazioni per una segnalazione di questo tipo? – janwijbrand

+0

Sì, avresti bisogno di un programma minimale che mostri il bug. – nickie

+5

Ho emesso una segnalazione di errore: http://bugs.python.org/issue18851 – janwijbrand

0

Altra soluzione, nel caso in cui non si gestiscano i file che sono stati aperti (ad es. Quando si costruisce un'API).

Ho trovato una soluzione al problema eseguendo chiamate API windll, per contrassegnare tutti i descrittori di file già aperti come "non ereditabili". Questo è un po 'un hack, e il Q & A è disponibile qui:

Howto: workaround of close_fds=True and redirect stdout/stderr on windows

sarà bypassare il bug di Python 2.7.

altra soluzione sarebbe quella di utilizzare Python 3.4+ :) E 'stato fissato

Problemi correlati