Pregunta

Esta es una pregunta de seguimiento de aquí.


Donde quiero ir

Me gustaría poder redirigir temporalmente el stdout en un archivo temperatorio, mientras que Python todavía puede imprimir en Stdout. Esto implicaría los siguientes pasos:

  1. Crear una copia de stdout (new)
  2. Crear un archivo temperador (tmp)
  3. Redirigir stdout a tmp
  4. Dígale a Python que use new como stdout
  5. Redireccionar tmp en el stdout "real"
  6. Dígale a Python que use el "real" Stdout nuevamente
  7. Leer y cerrar tmp

Implementación

Traté de implementar lo anterior de la siguiente manera:

import os
import subprocess
import sys

#A function that calls an external process to print to stdout as well as
#a python print to pythons stdout.
def Func(s, p = False):
    subprocess.call('echo "{0}"'.format(s), shell = True)
    if p:
        print "print"

sil = list() # <-- Some list to store the content of the temp files

print "0.1" # Some testing of the
Func("0.2") # functionality

new = os.dup(1)    # Create a copy of stdout (new)
tmp = os.tmpfile() # Create a temp file (tmp)

os.dup2(tmp.fileno(), 1)            # Redirect stdout into tmp
sys.stdout = os.fdopen(new, 'w', 0) # Tell python to use new as stdout

Func("0.3", True) # <--- This should print "0.3" to the temp file and "print" to stdout

os.dup2(new, 1)                   # Redirect tmp into "real" stdout
sys.stdout = os.fdopen(1, 'w', 0) # Tell python to use "real" stdout again

# Read and close tmp
tmp.flush()
tmp.seek(0, os.SEEK_SET)
sil.append(tmp.read())
tmp.close()

Me gustaría tomar un pequeño descanso aquí para resumir.
La salida para consolar hasta aquí debe leer:

0.1
0.2
print

tiempo sil Debería verse así: ['0.3\n']. Entonces, todo funciona como un encanto hasta aquí. Sin embargo, si rehato el guión de arriba de nuevo como así:

print "1.1" # Some testing of the
Func("1.2") # functionality

new = os.dup(1)    # Create a copy of stdout (new)
tmp = os.tmpfile() # Create a temp file (tmp)

os.dup2(tmp.fileno(), 1)            # Redirect stdout into tmp
sys.stdout = os.fdopen(new, 'w', 0) # Tell python to use new as stdout

# This should print "0.3" to the temp file and "print" to stdout and is the crucial point!
Func("1.3", True) 

os.dup2(new, 1)                   # Redirect tmp into "real" stdout
sys.stdout = os.fdopen(1, 'w', 0) # Tell python to use "real" stdout again

# Read and close tmp
tmp.flush()
tmp.seek(0, os.SEEK_SET)
sil.append(tmp.read())

Se produce un error y la salida se ve así:

1.1
1.2
/bin/sh: line 0: echo: write error: Bad file descriptor
print

tiempo sil lee: ['0.3\n', ''].

En otras palabras: el segundo Func("1.3", True) no puede escribir en el archivo temp.

Preguntas

  1. En primer lugar, me gustaría saber por qué mi guión no funciona como si quiero que funcione. Es decir, ¿por qué solo es posible en la primera mitad del script escribir en el archivo TEMP?
  2. Todavía estoy un poco perplejo por el uso de dup y dup2. Si bien creo que entiendo cómo la redirección de Stdout a un archivo temporal está funcionando, ahora sé por qué os.dup2(new, 1) está haciendo lo que está haciendo. Tal vez la respuesta podría explicar qué dup y dup2S en mi guión está haciendo ^^
¿Fue útil?

Solución

La razón por la que obtiene un "descriptor de archivo malo" es que el recolector de basura cierra el stdout fd para usted. Considere estas dos líneas:

sys.stdout = os.fdopen(1, 'w', 0)    # from first part of your script
...
sys.stdout = os.fdopen(new, 'w', 0)  # from second part of your script

Ahora, cuando los segundos de esos dos se ejecutan, el recuento de referencia del objeto del primer archivo cae a cero y el recolector de basura lo destruye. Los objetos de archivo cierran su FD asociado cuando se destruyen, y ese FD es 1 = STDOUT. Entonces debes tener mucho cuidado con la forma en que destruyes los objetos creados con os.fdopen.

Aquí hay un pequeño ejemplo para mostrar el problema. os.fstat se usa como una función de ejemplo que desencadena el error del "descriptor del archivo malo" cuando lo pasa un FD cerrado.

import os
whatever = os.fdopen(1, 'w', 0)
os.fstat(1)
del whatever
os.fstat(1)

De hecho, tengo un gerente de contexto que creo que hace exactamente (o casi al menos, en mi caso, necesito un Tempfile con nombre) lo que está buscando. Puede ver que reutiliza el objeto original sys.stdout para evitar el estrecho problemático.

import sys
import tempfile
import os

class captured_stdout:
    def __init__(self):
        self.prevfd = None
        self.prev = None

    def __enter__(self):
        F = tempfile.NamedTemporaryFile()
        self.prevfd = os.dup(sys.stdout.fileno())
        os.dup2(F.fileno(), sys.stdout.fileno())
        self.prev = sys.stdout
        sys.stdout = os.fdopen(self.prevfd, "w")
        return F

    def __exit__(self, exc_type, exc_value, traceback):
        os.dup2(self.prevfd, self.prev.fileno())
        sys.stdout = self.prev

## 
## Example usage
##

## here is a hack to print directly to stdout
import ctypes
libc=ctypes.LibraryLoader(ctypes.CDLL).LoadLibrary("libc.so.6")
def directfdprint(s):
    libc.write(1, s, len(s))


print("I'm printing from python before capture")
directfdprint("I'm printing from libc before captrue\n")

with captured_stdout() as E:
    print("I'm printing from python in capture")
    directfdprint("I'm printing from libc in capture\n")

print("I'm printing from python after capture")
directfdprint("I'm printing from libc after captrue\n")

print("Capture contains: " + repr(file(E.name).read()))
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top