dup, dup2, tmpfile and stdout in python

2024/10/5 7:28:36

This is a follow up question from here.


Where I want do go

I would like to be able to temporarily redirect the stdout into a temp file, while python still is able to print to stdout. This would involve the following steps:

  1. Create a copy of stdout (new)
  2. Create a temp file (tmp)
  3. Redirect stdout into tmp
  4. Tell python to use new as stdout
  5. Redirect tmp into the "real" stdout
  6. Tell python to use the "real" stdout again
  7. Read and close tmp

Implementation

I tried to implement the above in the following way:

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 filesprint "0.1" # Some testing of the
Func("0.2") # functionalitynew = 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 stdoutFunc("0.3", True) # <--- This should print "0.3" to the temp file and "print" to stdoutos.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()

I would like to take a little break here to summarize.
The output to console up until here should read:

0.1
0.2
print

while sil should look like this: ['0.3\n']. So everything is working like a charm up until here. However, if I redo the script above again like so:

print "1.1" # Some testing of the
Func("1.2") # functionalitynew = 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())

an error occurs and the output looks like this:

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

while sil reads: ['0.3\n', ''].

In other words: the second Func("1.3", True) is not able to write to the temp file.

Questions

  1. First of all, I would like to know why my script is not working like I want it to work. Meaning, why is it only possible in the first half of the script to write to the temp file?
  2. I am still a little puzzled by the usage of dup and dup2. While I think I understand how the redirection of stdout into a temp file is working I totally do now know why os.dup2(new, 1) is doing what it is doing. Maybe the answer could elaborate on what all the dup and dup2s in my script are doing^^
Answer

The reason you get a "bad file descriptor" is that the garbage collector closes the stdout FD for you. Consider these two lines:

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

Now when the second of those two are executed the first file object's reference count drops to zero and the garbage collector destroys it. File objects close their associated fd when destructed, and that fd happens to be 1 = stdout. So you need to be very careful with how you destroy objects created with os.fdopen.

Here is a small example to show the problem. os.fstat is just used as an example function that triggers the "Bad file descriptor" error when you pass it an closed fd.

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

I actually happen to have a context manager that I think does exactly (or almost atleast, in my case I happen need a named tempfile) what you are looking for. You can see that it reuses the original sys.stdout object to avoid the close problematic.

import sys
import tempfile
import osclass captured_stdout:def __init__(self):self.prevfd = Noneself.prev = Nonedef __enter__(self):F = tempfile.NamedTemporaryFile()self.prevfd = os.dup(sys.stdout.fileno())os.dup2(F.fileno(), sys.stdout.fileno())self.prev = sys.stdoutsys.stdout = os.fdopen(self.prevfd, "w")return Fdef __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()))
https://en.xdnf.cn/q/70514.html

Related Q&A

How do I write a Class-Based Django Validator?

Im using Django 1.8.The documentation on writing validators has an example of a function-based validator. It also says the following on using a class:You can also use a class with a __call__() method f…

python synthesize midi with fluidsynth

I cant import fluidsynth. [Maybe theres an better module?]Im trying to synthesize midi from python or pygame. I can send midi events from pygame. Im using mingus, and it seemed pyfluidsynth would be g…

Convenient way to handle deeply nested dictionary in Python

I have a deeply nested dictionary in python thats taking up a lot of room. Is there a way to abbreviate something like this master_dictionary[sub_categories][sub_cat_name][attributes][attribute_name][s…

Running tests against existing database using pytest-django

Does anybody know how to run Django Tests using pytest-django against an existing (e.g. production) database? I know that in general, this is not what unit tests are supposed to do, but in my case, I…

Python, Timeout of Try, Except Statement after X number of seconds?

Ive been searching on this but cant seem to find an exact answer (most get into more complicated things like multithreading, etc), I just want to do something like a Try, Except statement where if the …

MemoryError while pickling data in python

I am trying to dump a dictionary into pickle format, using dump command provided in python. The file size of the dictionary is around 150 mb, but an exception occurs when only 115 mb of the file is dum…

numpy dimensions

Im a newbie to Numpy and trying to understand the basic question of what is dimension,I tried the following commands and trying to understand why the ndim for last 2 arrays are same?>>> a= ar…

DataFrame Plot: how to sort X axis

I am plotting some counts from a field of dataframe (pandas) and I found that the X axis is sorted by the counts (descending order). Instead is it possible to sort by the alphabetical order of the fiel…

In-place custom object unpacking different behavior with __getitem__ python 3.5 vs python 3.6

a follow-up question on this question: i ran the code below on python 3.5 and python 3.6 - with very different results:class Container:KEYS = (a, b, c)def __init__(self, a=None, b=None, c=None):self.a …

Different ways of using __init__ for PyQt4

So... Im working on trying to move from basic Python to some GUI programming, using PyQt4. Im looking at a couple different books and tutorials, and they each seem to have a slightly different way of k…