2013-08-26 5 views
11

Eine interne Bibliothek, die subprocess.Popen() verwendet, begann die automatischen Tests fehlgeschlagen, wenn wir von Python 2.7.3 auf Python 2.7.5 aktualisiert. Diese Bibliothek wird in einer Umgebung mit Threads verwendet. Nach dem Debuggen des Problems konnte ich ein kurzes Python-Skript erstellen, das den Fehler wie in den fehlgeschlagenen Tests zeigt.Popen von Subprozess schließt stdout/stderr filedescriptors, die in einem anderen Thread verwendet werden, wenn Popen Fehler

Dies ist das Skript (genannt "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() 

Hinweis: dieses Skript mit einem IOError nicht beendet hat, wenn sie von Python 2.7.3 lief. Es schlägt mindestens 50% der Zeiten fehl, wenn es von Python 2.7.5 ausgeführt wurde (beide auf derselben Ubuntu 12.04 64-Bit-VM).

Der Fehler, die 2.7.5 auf Python angehoben wird, ist dies:

/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 

Wenn das Subprozess Modul aus Python 2.7.3 bis 2.7.5 Python I die Popen siehe Vergleichen() 's __init __() call schließt nun explizit die stdin, stdout und stderr Dateideskriptoren für den Fall, dass die Ausführung des Befehls fehlschlägt. Dies scheint ein beabsichtigter Fix zu sein, der in Python 2.7.4 angewendet wurde, um undichte Dateideskriptoren zu verhindern (http://hg.python.org/cpython/file/ab05e7dd2788/Misc/NEWS#l629).

Die diff zwischen Python 2.7.3 und Python 2.7.5, die zu diesem Thema relevant zu sein scheinen, ist in dem 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 

Ich glaube, ich drei Fragen:

1) Stimmt es, dass es grundsätzlich möglich ist, subprocess.Popen mit PIPE für stdin, stdout und stderr in einer Thread-Umgebung zu verwenden?

2) Wie verhindere ich, dass die Dateideskriptoren für stdin, stdout und stderr geschlossen werden, wenn Popen() in einem der Threads fehlschlägt?

3) Mache ich hier etwas falsch?

+0

Coole Sache ist, dass dies in Python fixiert ist 3.4+ – Cukic0d

Antwort

7

Ich möchte Ihre Fragen beantworten mit:

  1. Ja.
  2. Sie sollten nicht müssen.
  3. Nr

Der Fehler tritt in der Tat in Python 2.7.4 als auch.

Ich denke, das ist ein Fehler im Bibliothekscode. Wenn Sie in Ihrem Programm eine Sperre hinzufügen und sicherstellen, dass die beiden Aufrufe von subprocess.Popen atomar ausgeführt werden, tritt der Fehler nicht auf.

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

Dies bedeutet, dass es aufgrund einiger Daten Rennen bei der Umsetzung der Popen höchstwahrscheinlich ist. Ich werde eine Vermutung riskieren: die Fehler bei der Umsetzung der pipe_cloexec sein können, genannt von _get_handles, die (in 2.7.4) ist:

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 

und der Kommentar warnt ausdrücklich darüber nicht atomar zu sein ... Diese führt definitiv zu einem Datenrennen, aber ohne Experimente weiß ich nicht, ob es das Problem verursacht.

+0

Dank. Wenn Ihre Einschätzung in der Tat stimmt, würde das bedeuten, dass ich einen Fehlerbericht für Python 2.7.5 einreichen würde und hoffe, dass wir irgendwie eine weitere Bugfix-Freigabe erhalten. Denkst du, meine erste Frage wäre genug Informationen für einen solchen Fehlerbericht? – janwijbrand

+0

Ja, Sie benötigen ein minimales Programm, das den Fehler manifestiert. – nickie

+5

Ich habe einen Bugreport veröffentlicht: http://bugs.python.org/issue18851 – janwijbrand

0

Andere Lösung, falls Sie die geöffneten Dateien nicht bearbeiten (z. B. beim Erstellen einer API).

Ich habe eine Problemumgehung gefunden, indem ich windll API-Aufrufe gemacht habe, um alle bereits geöffneten Dateideskriptoren als "nicht vererbbar" zu markieren. Dies ist etwas ein Hack, und der Q-& A finden Sie hier:

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

Es wird die Python 2.7 Fehler umgehen.

Andere Lösung wäre Python zu verwenden 3.4+ :) Es wurde gefixt