Mathieu Fenniak's Weblog

2004/08/26

DevEnv vs. the Programmer

Filed under: programming, python — admin @ 3:57 pm

How can you capture the console output of a program, when it buffers that output if you’re not using a console to view it? This was a problem run into when building an automation tool for MS Visual Studio .NET. In the end, the programmer subjugated his tool (as it should be) by beating it over the head with a pipe.

A few of us programmers with in the unfortunately unfriendly environment of MS Windows. It might look pretty and have lots of applications written for it, but it’s basically an unfriendly environment for a software developer. Even MS Visual Studio .NET can be unfriendly to a developer, which is unfortunate since it’s the one program you’d expect would be really friendly.

Visual Studio allows you to provide command-line options which start a software build. Running inside a command prompt, all you need to do is pass a solution file and a build configuration to the program, and you’re off. In fact, Visual Studio even gives you more command-line flexibility by providing two executables, devenv.com and devenv.exe – the former will tend towards printing console output all the time, while the latter will avoid it if a build log file is provided instead.

In the creation of a complete build tool, I wanted to run devenv.com and capture the output so I could display the progress to a user. That’s when it became tough. Running the executable through os.popen (or any other popen function) didn’t accomplish what I wanted – the output being printed to the console (and now being read through a pipe) was buffered inside the devenv process and only printed after the build was completed. Clearly this didn’t accomplish the goal of providing a progress display for the user.

devenv.com provides an option which I thought might have some promise: /out. This writes the build output to a specified file. Great! All I need to do is start it writing to a file, and read through the file at the same time. I wasn’t sure of the implementation details, but it seemed feasible. Unfortunately, the devenv process locks the output file exclusively. Python’s open() was unable to read it, and even trying to find obscure parameters to win32file’s functions failed to give me the necessary access to the file.

In the UNIX world, the solution would be obvious. Create a pipe, and write the build output into the pipe while reading the pipe. In the Windows world though, a pipe is not a filesystem object. It can’t be created in a specific location, and so devenv wouldn’t be able to open it like a normal file and write to it. I considered for a while that there are a bunch of standard reserved file names, like CON and PRN. Might one of them help me? Could one of them be used to connect to a pipe? Well, no. Not really. They’re ancient history, a relic from years gone past, and they don’t have any concept of a pipe.

I started digging around for more information about named pipes, which seemed to be the prefered mechanism for IPC in Windows software. Could a named pipe be referenced through a file location? Yes, it can! \\%(host)s\pipe\%(name)s refers to the named pipe name on the host host. And as a bonus, the host . refers to the local machine at all times. Now I finally have a plan of action: Create a named pipe, make devenv write to \\.\pipe\buildOutput, and read the output on the fly.

In the end, I wrapped the named pipe code into module, NamedPipe, and the code to read devenv output on the fly was easy:

from NamedPipe import AnonymousNamedPipeReader

pipe = AnonymousNamedPipeReader()

# Application command line...
# (build application cmd line, devenv.com x.sln /build Release, etc..
# {code omitted}
cmd = cmd + r' /out \\.\pipe\%s' % pipe.name

# Okay, one of us needs to loop and accept a pipe connection, read
# data, display it to the user, and so on.
# The other of us needs to run the build command.
class ExecThread(threading.Thread):
    def __init__(self, cmdLine):
        threading.Thread.__init__(self)
        self.cmdLine = cmdLine
    def run(self):
        self.retval = os.system(self.cmdLine)
thread = ExecThread(cmd)
thread.start()

buildLog = ""
line = ""

for data in pipe:
    buildLog += data
    # {code omitted - display output on the fly}

Now, obviously this code snippet has left out all the magic. It’s a bit long and boring, so I thought maybe you’d just like a link to NamedPipe.py instead. Through the magic of functions like CreateNamedPipe and ConnectNamedPipe, you can read data being written to a file on the fly. It even works when the writer is a jerk, locking the file.

6 Comments

  1. I’ve implemented this to drive devenv.com in Devstudio 2005 and I don’t get on the fly progress, it seems live devenv caches up all of it’s logging and spits it out all at once. Is this your current experience? Or do you think I’ve implemented something wrong?

    Comment by visionep — 2006/09/21 @ 9:07 pm

  2. visionep — I haven’t tried it with Visual Studio 2005. My experience was with Visual Studio 2003, and I don’t believe I had the problem you describe. It is possible that the behavior of the software has changed between versions.

    Comment by Mathieu Fenniak — 2006/09/21 @ 9:17 pm

  3. I’ve worked around the problem with 2005 caching up all of its output by calling vcbuild.exe. You can’t control what you are building as accurately as you can with devenv, but it gets the job done with live progress.

    Comment by visionep — 2006/12/13 @ 7:29 pm

  4. Can you post the complete script that you use to build from the command line?

    Comment by jshuler — 2007/03/08 @ 11:08 am

  5. I created and connected to a named pipe as well, passing in the same pipe name with the /Out command line switch. The output did not come through the pipe until the very end. I have read, however, that this behavior is “fixed” in SP1 (see http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=1411143&SiteID=1).

    Comment by Jay — 2007/05/30 @ 7:56 am

  6. The following piece of code works for me with Visual Studio 2005 SP1. Note that I had to use a slightly different format for the pipe name (r”\\.\pipe\%s”):

    from subprocess import Popen
    from NamedPipe import AnonymousNamedPipeReader

    DEVENV_EXE=r”C:\Program Files\Microsoft Visual Studio 8\Common7\IDE\devenv.exe”
    solution = “mysolution.sln”
    configuration = “Debug|Win32″

    pipe = AnonymousNamedPipeReader()
    process = Popen([DEVENV_EXE, solution, "/Build", configuration, "/Out", r"\\.\pipe\%s" % pipe.name])
    for line in pipe:
    print line,

    Comment by Mark — 2007/07/27 @ 2:41 pm

RSS feed for comments on this post. TrackBack URL

Sorry, the comment form is closed at this time.

Powered by WordPress