DUP, DUP2, TMPFILE y STDOT en Python
-
27-10-2019 - |
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:
- Crear una copia de stdout (
new
) - Crear un archivo temperador (
tmp
) - Redirigir stdout a
tmp
- Dígale a Python que use
new
como stdout - Redireccionar
tmp
en el stdout "real" - Dígale a Python que use el "real" Stdout nuevamente
- 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
- 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?
- Todavía estoy un poco perplejo por el uso de
dup
ydup2
. 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
ydup2
S en mi guión está haciendo ^^
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()))