An interesting problem in Python

I like to tell stories about poorly written code.  As a programmer you come across it all the time, and good programmers work really hard to make sure everything they write is both clean and as free from bugs as you can make it.  There is a reason, though, that we spend so much time fixing bugs and that’s because it can be extremely difficult to write bug free code.  This problem that I encountered recently at work is one of those bugs that just sneaks up on you and punches you in the face… see if you can find the bug:

def createProc(acmd):
    dn = open("/dev/null")
    return subprocess.call(acmd, stdout=dn, stderr=dn)

I honestly did not see it at first and debugging it was a real pain, but if you study it and maybe take a quick refresh of the Python documentation you’ll see it.   In this case the program being spawned was crashing infrequently, and it would only fail if the program it was calling had an error.  Here’s how this function should have been written:

def createProc(acmd):
    dn = open("/dev/null", "w")
    return subprocess.call(acmd, stdout=dn, stderr=dn)

By now it should be obvious.  Python’s default open() mode is “r”ead.  This program was normally silent, but whenever it had a problem it would write an error message stderr and then quit.   The first time it tried to write to stderr it would throw an exception within the subprocess context, without being raised into the calling process.  If you tried to just open a file in Python without setting “w”rite mode and write to it the interpreter would throw an IOError telling you that the file handle is invalid but because of the call to fork() you would never see that exception in the calling function… just a return code of 1.

I actually found this problem while trying to track down another issue:

def createProc_bad(acmd):
    return subprocess.call(acmd, stdout=subprocess.PIPE,
                                 stderr=subprocess.PIPE)

This also seems rather benign.  Well, as benign as something can be when you are forking another process but in this case the process would hang!   This time the problem was a little easier to track down.   I’ve done a lot of work on pipes and I know enough about Python’s implementation to know that it was probably blocking on the pipe because we were never reading from it.  Once that pipe buffer fills up the process will block waiting to write to it and the application wouldn’t be able to make progress.   Here’s a good solution if you may need the data that would be written to those pipes and don’t want to worry about reading from it as the process progresses:

def createProc_better(acmd):
    proc = subprocess.Popen(acmd, stdout=PIPE, stderr=PIPE)
    (sout, serr) = proc.communicate()
    rc = proc.wait()
    return (rc, (sout, serr))

One has to be careful with this implementation, though.   Python will store this information in heap and it will quickly blow up the interpreter’s memory usage.   If you didn’t care about the output you could just redirect it to /dev/null as shown above.

Leave a comment

0 Comments.

Leave a Reply


[ Ctrl + Enter ]