]> git.saurik.com Git - wxWidgets.git/blobdiff - wxPython/samples/ide/activegrid/tool/process.py
Added the ActiveGrid IDE as a sample application
[wxWidgets.git] / wxPython / samples / ide / activegrid / tool / process.py
diff --git a/wxPython/samples/ide/activegrid/tool/process.py b/wxPython/samples/ide/activegrid/tool/process.py
new file mode 100644 (file)
index 0000000..a521f0c
--- /dev/null
@@ -0,0 +1,2364 @@
+#!/usr/bin/env python
+# Copyright (c) 2002-2003 ActiveState
+# See LICENSE.txt for license details.
+""" Contents of LICENSE.txt:
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+"""
+r"""
+    Python interface for process control.
+
+    This module defines three Process classes for spawning,
+    communicating and control processes. They are: Process, ProcessOpen,
+    ProcessProxy. All of the classes allow one to specify the command (cmd),
+    starting working directory (cwd), and environment to create for the
+    new process (env) and to "wait" for termination of the child and
+    "kill" the child.
+
+    Process:
+        Use this class to simply launch a process (either a GUI app or a
+        console app in a new console) with which you do not intend to
+        communicate via it std handles.
+
+    ProcessOpen:
+        Think of this as a super version of Python's os.popen3() method.
+        This spawns the given command and sets up pipes for
+        stdin/stdout/stderr which can then be used to communicate with
+        the child.
+
+    ProcessProxy:
+        This is a heavy-weight class that, similar to ProcessOpen,
+        spawns the given commands and sets up pipes to the child's
+        stdin/stdout/stderr. However, it also starts three threads to
+        proxy communication between each of the child's and parent's std
+        handles. At the parent end of this communication are, by
+        default, IOBuffer objects. You may specify your own objects here
+        (usually sub-classing from IOBuffer, which handles some
+        synchronization issues for you). The result is that it is
+        possible to have your own IOBuffer instance that gets, say, a
+        .write() "event" for every write that the child does on its
+        stdout.
+
+        Understanding ProcessProxy is pretty complex. Some examples
+        below attempt to help show some uses. Here is a diagram of the
+        comminucation:
+
+                            <parent process>
+               ,---->->->------'   ^   `------>->->----,
+               |                   |                   v
+           IOBuffer             IOBuffer            IOBuffer        
+           (p.stdout)           (p.stderr)          (p.stdin)
+               |                   |                   |
+           _OutFileProxy        _OutFileProxy       _InFileProxy
+           thread               thread              thread
+               |                   ^                   |
+               `----<-<-<------,   |   ,------<-<-<----'
+                            <child process>
+
+    Usage:
+        import process
+        p = process.<Process class>(cmd='echo hi', ...)
+        #... use the various methods and attributes
+
+    Examples:
+      A simple 'hello world':
+        >>> import process
+        >>> p = process.ProcessOpen(['echo', 'hello'])
+        >>> p.stdout.read()
+        'hello\r\n'
+        >>> p.wait()   # .wait() returns the child's exit status
+        0
+
+      Redirecting the stdout handler:
+        >>> import sys
+        >>> p = process.ProcessProxy(['echo', 'hello'], stdout=sys.stdout)
+        hello
+
+      Using stdin (need to use ProcessProxy here because it defaults to
+      text-mode translation on Windows, ProcessOpen does not support
+      this):
+        >>> p = process.ProcessProxy(['sort'])
+        >>> p.stdin.write('5\n')
+        >>> p.stdin.write('2\n')
+        >>> p.stdin.write('7\n')
+        >>> p.stdin.close()
+        >>> p.stdout.read()
+        '2\n5\n7\n'
+
+      Specifying environment variables:
+        >>> p = process.ProcessOpen(['perl', '-e', 'print $ENV{FOO}'])
+        >>> p.stdout.read()
+        ''
+        >>> p = process.ProcessOpen(['perl', '-e', 'print $ENV{FOO}'],
+        ...                         env={'FOO':'bar'})
+        >>> p.stdout.read()
+        'bar'
+
+      Killing a long running process (On Linux, to poll you must use
+      p.wait(os.WNOHANG)):
+        >>> p = ProcessOpen(['perl', '-e', 'while (1) {}'])
+        >>> try:
+        ...     p.wait(os.WNOHANG)  # poll to see if is process still running
+        ... except ProcessError, ex:
+        ...     if ex.errno == ProcessProxy.WAIT_TIMEOUT:
+        ...             print "process is still running"
+        ...
+        process is still running
+        >>> p.kill(42)
+        >>> p.wait()
+        42
+
+      Providing objects for stdin/stdout/stderr:
+        XXX write this, mention IOBuffer subclassing.
+"""
+#TODO:
+#   - Discuss the decision to NOT have the stdout/stderr _OutFileProxy's
+#     wait for process termination before closing stdin. It will just
+#     close stdin when stdout is seen to have been closed. That is
+#     considered Good Enough (tm). Theoretically it would be nice to
+#     only abort the stdin proxying when the process terminates, but
+#     watching for process termination in any of the parent's thread
+#     adds the undesired condition that the parent cannot exit with the
+#     child still running. That sucks.
+#     XXX Note that I don't even know if the current stdout proxy even
+#         closes the stdin proxy at all.
+#   - DavidA: if I specify "unbuffered" for my stdin handler (in the
+#     ProcessProxy constructor) then the stdin IOBuffer should do a
+#     fparent.read() rather than a fparent.readline(). TrentM: can I do
+#     that? What happens?
+#
+
+import os
+import sys
+import threading
+import types
+import pprint 
+if sys.platform.startswith("win"):
+    import msvcrt
+    import win32api
+    import win32file
+    import win32pipe
+    import pywintypes
+    import win32process
+    import win32event
+    # constants pulled from win32con to save memory
+    VER_PLATFORM_WIN32_WINDOWS = 1
+    CTRL_BREAK_EVENT = 1
+    SW_SHOWDEFAULT = 10
+    WM_CLOSE = 0x10
+    DUPLICATE_SAME_ACCESS = 2
+    
+else:
+    import signal
+
+
+#---- exceptions
+
+class ProcessError(Exception):
+    def __init__(self, msg, errno=-1):
+        Exception.__init__(self, msg)
+        self.errno = errno
+
+
+#---- internal logging facility
+
+class Logger:
+    DEBUG, INFO, WARN, ERROR, FATAL = range(5)
+    def __init__(self, name, level=None, streamOrFileName=sys.stderr):
+        self.name = name
+        if level is None:
+            self.level = self.WARN
+        else:
+            self.level = level
+        if type(streamOrFileName) == types.StringType:
+            self.stream = open(streamOrFileName, 'w')
+            self._opennedStream = 1
+        else:
+            self.stream = streamOrFileName
+            self._opennedStream = 0
+    def __del__(self):
+        if self._opennedStream:
+            self.stream.close()
+    def _getLevelName(self, level):
+        levelNameMap = {
+            self.DEBUG: "DEBUG",
+            self.INFO: "INFO",
+            self.WARN: "WARN",
+            self.ERROR: "ERROR",
+            self.FATAL: "FATAL",
+        }
+        return levelNameMap[level]
+    def log(self, level, msg, *args):
+        if level < self.level:
+            return
+        message = "%s: %s:" % (self.name, self._getLevelName(level).lower())
+        message = message + (msg % args) + "\n"
+        self.stream.write(message)
+        self.stream.flush()
+    def debug(self, msg, *args):
+        self.log(self.DEBUG, msg, *args)
+    def info(self, msg, *args):
+        self.log(self.INFO, msg, *args)
+    def warn(self, msg, *args):
+        self.log(self.WARN, msg, *args)
+    def error(self, msg, *args):
+        self.log(self.ERROR, msg, *args)
+    def fatal(self, msg, *args):
+        self.log(self.FATAL, msg, *args)
+
+# Loggers:
+#   - 'log' to log normal process handling
+#   - 'logres' to track system resource life
+#   - 'logfix' to track wait/kill proxying in _ThreadFixer
+if 1:   # normal/production usage
+    log = Logger("process", Logger.WARN)
+else:   # development/debugging usage
+    log = Logger("process", Logger.DEBUG, sys.stdout)
+if 1:   # normal/production usage
+    logres = Logger("process.res", Logger.WARN)
+else:   # development/debugging usage
+    logres = Logger("process.res", Logger.DEBUG, sys.stdout)
+if 1:   # normal/production usage
+    logfix = Logger("process.waitfix", Logger.WARN)
+else:   # development/debugging usage
+    logfix = Logger("process.waitfix", Logger.DEBUG, sys.stdout)
+
+
+
+#---- globals
+
+_version_ = (0, 5, 0)
+
+# List of registered processes (see _(un)registerProcess).
+_processes = []
+
+
+
+#---- internal support routines
+
+def _escapeArg(arg):
+    """Escape the given command line argument for the shell."""
+    #XXX There is a probably more that we should escape here.
+    return arg.replace('"', r'\"')
+
+
+def _joinArgv(argv):
+    r"""Join an arglist to a string appropriate for running.
+
+        >>> import os
+        >>> _joinArgv(['foo', 'bar "baz'])
+        'foo "bar \\"baz"'
+    """
+    cmdstr = ""
+    for arg in argv:
+        if ' ' in arg or ';' in arg:
+            cmdstr += '"%s"' % _escapeArg(arg)
+        else:
+            cmdstr += _escapeArg(arg)
+        cmdstr += ' '
+    if cmdstr.endswith(' '): cmdstr = cmdstr[:-1]  # strip trailing space
+    return cmdstr
+
+
+def _getPathFromEnv(env):
+    """Return the PATH environment variable or None.
+
+    Do the right thing for case sensitivity per platform.
+    XXX Icky. This guarantee of proper case sensitivity of environment
+        variables should be done more fundamentally in this module.
+    """
+    if sys.platform.startswith("win"):
+        for key in env.keys():
+            if key.upper() == "PATH":
+                return env[key]
+        else:
+            return None
+    else:
+        if env.has_key("PATH"):
+            return env["PATH"]
+        else:
+            return None
+
+
+def _whichFirstArg(cmd, env=None):
+    """Return the given command ensuring that the first arg (the command to
+    launch) is a full path to an existing file.
+
+    Raise a ProcessError if no such executable could be found.
+    """
+    # Parse out the first arg.
+    if cmd.startswith('"'):
+        # The .replace() is to ensure it does not mistakenly find the
+        # second '"' in, say (escaped quote):
+        #           "C:\foo\"bar" arg1 arg2
+        idx = cmd.replace('\\"', 'XX').find('"', 1)
+        if idx == -1:
+            raise ProcessError("Malformed command: %r" % cmd)
+        first, rest = cmd[1:idx], cmd[idx+1:]
+        rest = rest.lstrip()
+    else:
+        if ' ' in cmd:
+            first, rest = cmd.split(' ', 1)
+        else:
+            first, rest = cmd, ""
+
+    # Ensure the first arg is a valid path to the appropriate file.
+    import which
+    if os.sep in first:
+        altpath = [os.path.dirname(first)]
+        firstbase = os.path.basename(first)
+        candidates = list(which.which(firstbase, path=altpath))
+    elif env:
+        altpath = _getPathFromEnv(env)
+        if altpath:
+            candidates = list(which.which(first, altpath.split(os.pathsep)))
+        else:
+            candidates = list(which.which(first))
+    else:
+        candidates = list(which.which(first))
+    if candidates:
+        return _joinArgv( [candidates[0]] ) + ' ' + rest
+    else:
+        raise ProcessError("Could not find an appropriate leading command "\
+                           "for: %r" % cmd)
+
+
+if sys.platform.startswith("win"):
+    def _SaferCreateProcess(appName,        # app name
+                            cmd,            # command line 
+                            processSA,      # process security attributes 
+                            threadSA,       # thread security attributes 
+                            inheritHandles, # are handles are inherited
+                            creationFlags,  # creation flags 
+                            env,            # environment
+                            cwd,            # current working directory
+                            si):            # STARTUPINFO pointer
+        """If CreateProcess fails from environment type inconsistency then
+        fix that and try again.
+        
+        win32process.CreateProcess requires that all environment keys and
+        values either be all ASCII or all unicode. Try to remove this burden
+        from the user of process.py.
+        """
+        isWin9x = win32api.GetVersionEx()[3] == VER_PLATFORM_WIN32_WINDOWS
+        # On Win9x all keys and values of 'env' must be ASCII (XXX
+        # Actually this is probably only true if the Unicode support
+        # libraries, which are not installed by default, are not
+        # installed). On other Windows flavours all keys and values of
+        # 'env' must all be ASCII *or* all Unicode. We will try to
+        # automatically convert to the appropriate type, issuing a
+        # warning if such an automatic conversion is necessary.
+
+        #XXX Komodo 2.0 Beta 1 hack. This requirement should be
+        #    pushed out to Komodo code using process.py. Or should it?
+        if isWin9x and env:
+            aenv = {}
+            for key, value in env.items():
+                aenv[str(key)] = str(value)
+            env = aenv
+        
+        log.debug("""\
+_SaferCreateProcess(appName=%r,
+                    cmd=%r,
+                    env=%r,
+                    cwd=%r)
+    os.getcwd(): %r
+""", appName, cmd, env, cwd, os.getcwd())
+        try:
+            hProcess, hThread, processId, threadId\
+                = win32process.CreateProcess(appName, cmd, processSA,
+                                             threadSA, inheritHandles,
+                                             creationFlags, env, cwd, si)
+        except TypeError, ex:
+            if ex.args == ('All dictionary items must be strings, or all must be unicode',):
+                # Try again with an all unicode environment.
+                #XXX Would be nice if didn't have to depend on the error
+                #    string to catch this.
+                #XXX Removing this warning for 2.3 release. See bug
+                #    23215. The right fix is to correct the PHPAppInfo
+                #    stuff to heed the warning.
+                #import warnings
+                #warnings.warn('env: ' + str(ex), stacklevel=4)
+                if isWin9x and env:
+                    aenv = {}
+                    try:
+                        for key, value in env.items():
+                            aenv[str(key)] = str(value)
+                    except UnicodeError, ex:
+                        raise ProcessError(str(ex))
+                    env = aenv
+                elif env:
+                    uenv = {}
+                    for key, val in env.items():
+                        uenv[unicode(key)] = unicode(val)
+                    env = uenv
+                hProcess, hThread, processId, threadId\
+                    = win32process.CreateProcess(appName, cmd, processSA,
+                                                 threadSA, inheritHandles,
+                                                 creationFlags, env, cwd,
+                                                 si)
+            else:
+                raise
+        return hProcess, hThread, processId, threadId
+
+
+# Maintain references to all spawned ProcessProxy objects to avoid hangs.
+#   Otherwise, if the user lets the a ProcessProxy object go out of
+#   scope before the process has terminated, it is possible to get a
+#   hang (at least it *used* to be so when we had the
+#   win32api.CloseHandle(<stdin handle>) call in the __del__() method).
+#   XXX Is this hang possible on Linux as well?
+# A reference is removed from this list when the process's .wait or
+# .kill method is called.
+# XXX Should an atexit() handler be registered to kill all curently
+#     running processes? Else *could* get hangs, n'est ce pas?
+def _registerProcess(process):
+    global _processes
+    log.info("_registerprocess(process=%r)", process)
+
+    # Clean up zombie processes.
+    #   If the user does not call .wait() or .kill() on processes then
+    #   the ProcessProxy object will not get cleaned up until Python
+    #   exits and _processes goes out of scope. Under heavy usage that
+    #   is a big memory waste. Cleaning up here alleviates that.
+    for p in _processes[:]: # use copy of _process, because we may modifiy it
+        try:
+            # poll to see if is process still running
+            if sys.platform.startswith("win"):
+                timeout = 0
+            else:
+                timeout = os.WNOHANG
+            p.wait(timeout)
+            _unregisterProcess(p)
+        except ProcessError, ex:
+            if ex.errno == ProcessProxy.WAIT_TIMEOUT:
+                pass
+            else:
+                raise
+        
+    _processes.append(process)
+
+def _unregisterProcess(process):
+    global _processes
+    log.info("_unregisterProcess(process=%r)", process)
+    try:
+        _processes.remove(process)
+        del process
+    except ValueError:
+        pass
+
+
+def _fixupCommand(cmd, env=None):
+    """Fixup the command string so it is launchable via CreateProcess.
+
+    One cannot just launch, say "python", via CreateProcess. A full path
+    to an executable is required. In general there are two choices:
+        1. Launch the command string via the shell. The shell will find
+           the fullpath to the appropriate executable. This shell will
+           also be able to execute special shell commands, like "dir",
+           which don't map to an actual executable.
+        2. Find the fullpath to the appropriate executable manually and
+           launch that exe.
+
+    Option (1) is preferred because you don't have to worry about not
+    exactly duplicating shell behaviour and you get the added bonus of
+    being able to launch "dir" and friends.
+
+    However, (1) is not always an option. Doing so when the shell is
+    command.com (as on all Win9x boxes) or when using WinNT's cmd.exe,
+    problems are created with .kill() because these shells seem to eat
+    up Ctrl-C's and Ctrl-Break's sent via
+    win32api.GenerateConsoleCtrlEvent().  Strangely this only happens
+    when spawn via this Python interface. For example, Ctrl-C get
+    through to hang.exe here:
+      C:\> ...\w9xpopen.exe "C:\WINDOWS\COMMAND.COM /c hang.exe"
+      ^C
+    but not here:
+      >>> p = ProcessOpen('hang.exe')
+      # This results in the same command to CreateProcess as
+      # above.
+      >>> p.kill()
+
+    Hence, for these platforms we fallback to option (2).  Cons:
+      - cannot spawn shell commands like 'dir' directly
+      - cannot spawn batch files
+    """
+    if sys.platform.startswith("win"):
+        # Fixup the command string to spawn.  (Lifted from
+        # posixmodule.c::_PyPopenCreateProcess() with some modifications)
+        comspec = os.environ.get("COMSPEC", None)
+        win32Version = win32api.GetVersion()
+        if comspec is None:
+            raise ProcessError("Cannot locate a COMSPEC environment "\
+                               "variable to use as the shell")
+        # Explicitly check if we are using COMMAND.COM.  If we
+        # are then use the w9xpopen hack.
+        elif (win32Version & 0x80000000L == 0) and\
+             (win32Version &        0x5L >= 5) and\
+             os.path.basename(comspec).lower() != "command.com":
+            # 2000/XP and not using command.com.
+            if '"' in cmd or "'" in cmd:
+                cmd = comspec + ' /c "%s"' % cmd
+            else:
+                cmd = comspec + ' /c ' + cmd
+        elif (win32Version & 0x80000000L == 0) and\
+             (win32Version &        0x5L  < 5) and\
+             os.path.basename(comspec).lower() != "command.com":
+            # NT and not using command.com.
+            try:
+                cmd = _whichFirstArg(cmd, env)
+            except ProcessError:
+                raise ProcessError("Could not find a suitable executable "\
+                    "to launch for '%s'. On WinNT you must manually prefix "\
+                    "shell commands and batch files with 'cmd.exe /c' to "\
+                    "have the shell run them." % cmd)
+        else:
+            # Oh gag, we're on Win9x and/or using COMMAND.COM. Use the
+            # workaround listed in KB: Q150956
+            w9xpopen = os.path.join(
+                os.path.dirname(win32api.GetModuleFileName(0)),
+                'w9xpopen.exe')
+            if not os.path.exists(w9xpopen):
+                # Eeek - file-not-found - possibly an embedding
+                # situation - see if we can locate it in sys.exec_prefix
+                w9xpopen = os.path.join(os.path.dirname(sys.exec_prefix),
+                                        'w9xpopen.exe')
+                if not os.path.exists(w9xpopen):
+                    raise ProcessError(\
+                        "Can not locate 'w9xpopen.exe' which is needed "\
+                        "for ProcessOpen to work with your shell or "\
+                        "platform.")
+            ## This would be option (1):
+            #cmd = '%s "%s /c %s"'\
+            #      % (w9xpopen, comspec, cmd.replace('"', '\\"'))
+            try:
+                cmd = _whichFirstArg(cmd, env)
+            except ProcessError:
+                raise ProcessError("Could not find a suitable executable "\
+                    "to launch for '%s'. On Win9x you must manually prefix "\
+                    "shell commands and batch files with 'command.com /c' "\
+                    "to have the shell run them." % cmd)
+            cmd = '%s "%s"' % (w9xpopen, cmd.replace('"', '\\"'))
+    return cmd
+
+class _FileWrapper:
+    """Wrap a system file object, hiding some nitpicky details.
+    
+    This class provides a Python file-like interface to either a Python
+    file object (pretty easy job), a file descriptor, or an OS-specific
+    file handle (e.g.  Win32 handles to file objects on Windows). Any or
+    all of these object types may be passed to this wrapper. If more
+    than one is specified this wrapper prefers to work with certain one
+    in this order:
+        - file descriptor (because usually this allows for
+          return-immediately-on-read-if-anything-available semantics and
+          also provides text mode translation on Windows)
+        - OS-specific handle (allows for the above read semantics)
+        - file object (buffering can cause difficulty for interacting
+          with spawned programs)
+
+    It also provides a place where related such objects can be kept
+    alive together to prevent premature ref-counted collection. (E.g. on
+    Windows a Python file object may be associated with a Win32 file
+    handle. If the file handle is not kept alive the Python file object
+    will cease to function.)
+    """
+    def __init__(self, file=None, descriptor=None, handle=None):
+        self._file = file
+        self._descriptor = descriptor
+        self._handle = handle
+        self._closed = 0
+        if self._descriptor is not None or self._handle is not None:
+            self._lineBuf = "" # to support .readline()
+
+    def __del__(self):
+        self.close()
+
+    def __getattr__(self, name):
+        """Forward to the underlying file object."""
+        if self._file is not None:
+            return getattr(self._file, name)
+        else:
+            raise ProcessError("no file object to pass '%s' attribute to"
+                               % name)
+
+    def _win32Read(self, nBytes):
+        try:
+            log.info("[%s] _FileWrapper.read: waiting for read on pipe",
+                     id(self))
+            errCode, text = win32file.ReadFile(self._handle, nBytes)
+        except pywintypes.error, ex:
+            # Ignore errors for now, like "The pipe is being closed.",
+            # etc. XXX There *may* be errors we don't want to avoid.
+            log.info("[%s] _FileWrapper.read: error reading from pipe: %s",
+                     id(self), ex)
+            return ""
+        assert errCode == 0,\
+               "Why is 'errCode' from ReadFile non-zero? %r" % errCode
+        if not text:
+            # Empty text signifies that the pipe has been closed on
+            # the parent's end.
+            log.info("[%s] _FileWrapper.read: observed close of parent",
+                     id(self))
+            # Signal the child so it knows to stop listening.
+            self.close()
+            return ""
+        else:
+            log.info("[%s] _FileWrapper.read: read %d bytes from pipe: %r",
+                     id(self), len(text), text)
+        return text
+
+    def read(self, nBytes=-1):
+        # nBytes <= 0 means "read everything"
+        #   Note that we are changing the "read everything" cue to
+        #   include 0, because actually doing
+        #   win32file.ReadFile(<handle>, 0) results in every subsequent
+        #   read returning 0, i.e. it shuts down the pipe.
+        if self._descriptor is not None:
+            if nBytes <= 0:
+                text, self._lineBuf = self._lineBuf, ""
+                while 1:
+                    t = os.read(self._descriptor, 4092)
+                    if not t:
+                        break
+                    else:
+                        text += t
+            else:
+                if len(self._lineBuf) >= nBytes:
+                    text, self._lineBuf =\
+                        self._lineBuf[:nBytes], self._lineBuf[nBytes:]
+                else:
+                    nBytesToGo = nBytes - len(self._lineBuf)
+                    text = self._lineBuf + os.read(self._descriptor,
+                                                   nBytesToGo)
+                    self._lineBuf = ""
+            return text
+        elif self._handle is not None:
+            if nBytes <= 0:
+                text, self._lineBuf = self._lineBuf, ""
+                while 1:
+                    t = self._win32Read(4092)
+                    if not t:
+                        break
+                    else:
+                        text += t
+            else:
+                if len(self._lineBuf) >= nBytes:
+                    text, self._lineBuf =\
+                        self._lineBuf[:nBytes], self._lineBuf[nBytes:]
+                else:
+                    nBytesToGo = nBytes - len(self._lineBuf)
+                    text, self._lineBuf =\
+                        self._lineBuf + self._win32Read(nBytesToGo), ""
+            return text
+        elif self._file is not None:
+            return self._file.read(nBytes)
+        else:   
+            raise "FileHandle.read: no handle to read with"
+
+    def readline(self):
+        if self._descriptor is not None or self._handle is not None:
+            while 1:
+                #XXX This is not portable to the Mac.
+                idx = self._lineBuf.find('\n')
+                if idx != -1:
+                    line, self._lineBuf =\
+                        self._lineBuf[:idx+1], self._lineBuf[idx+1:]
+                    break
+                else:
+                    lengthBefore = len(self._lineBuf)
+                    t = self.read(4092)
+                    if len(t) <= lengthBefore: # no new data was read
+                        line, self._lineBuf = self._lineBuf, ""
+                        break
+                    else:
+                        self._lineBuf += t
+            return line
+        elif self._file is not None:
+            return self._file.readline()
+        else:
+            raise "FileHandle.readline: no handle to read with"
+
+    def readlines(self):
+        if self._descriptor is not None or self._handle is not None:
+            lines = []
+            while 1:
+                line = self.readline()
+                if line:
+                    lines.append(line)
+                else:
+                    break
+            return lines
+        elif self._file is not None:
+            return self._file.readlines()
+        else:
+            raise "FileHandle.readline: no handle to read with"
+
+    def write(self, text):
+        if self._descriptor is not None:
+            os.write(self._descriptor, text)
+        elif self._handle is not None:
+            try:
+                errCode, nBytesWritten = win32file.WriteFile(self._handle, text)
+            except pywintypes.error, ex:
+                # Ingore errors like "The pipe is being closed.", for
+                # now.
+                log.info("[%s] _FileWrapper.write: error writing to pipe, "\
+                         "ignored", id(self))
+                return
+            assert errCode == 0,\
+                   "Why is 'errCode' from WriteFile non-zero? %r" % errCode
+            if not nBytesWritten:
+                # No bytes written signifies that the pipe has been
+                # closed on the child's end.
+                log.info("[%s] _FileWrapper.write: observed close of pipe",
+                         id(self))
+                return
+            else:
+                log.info("[%s] _FileWrapper.write: wrote %d bytes to pipe: %r",
+                         id(self), len(text), text)
+        elif self._file is not None:
+            self._file.write(text)
+        else:   
+            raise "FileHandle.write: nothing to write with"
+
+    def close(self):
+        """Close all associated file objects and handles."""
+        log.debug("[%s] _FileWrapper.close()", id(self))
+        if not self._closed:
+            self._closed = 1
+            if self._file is not None:
+                log.debug("[%s] _FileWrapper.close: close file", id(self))
+                self._file.close()
+                log.debug("[%s] _FileWrapper.close: done file close", id(self))
+            if self._descriptor is not None:
+                try:
+                    os.close(self._descriptor)
+                except OSError, ex:
+                    if ex.errno == 9:
+                        # Ignore: OSError: [Errno 9] Bad file descriptor
+                        # XXX *Should* we be ignoring this? It appears very
+                        #     *in*frequently in test_wait.py.
+                        log.debug("[%s] _FileWrapper.close: closing "\
+                                  "descriptor raised OSError", id(self))
+                    else:
+                        raise
+            if self._handle is not None:
+                log.debug("[%s] _FileWrapper.close: close handle", id(self))
+                try:
+                    win32api.CloseHandle(self._handle)
+                except win32api.error:
+                    log.debug("[%s] _FileWrapper.close: closing handle raised",
+                              id(self))
+                    pass
+                log.debug("[%s] _FileWrapper.close: done closing handle",
+                          id(self))
+
+    def __repr__(self):
+        return "<_FileWrapper: file:%r fd:%r os_handle:%r>"\
+               % (self._file, self._descriptor, self._handle)
+
+
+class _CountingCloser:
+    """Call .close() on the given object after own .close() is called
+    the precribed number of times.
+    """
+    def __init__(self, objectsToClose, count):
+        """
+        "objectsToClose" is a list of object on which to call .close().
+        "count" is the number of times this object's .close() method
+            must be called before .close() is called on the given objects.
+        """
+        self.objectsToClose = objectsToClose
+        self.count = count
+        if self.count <= 0:
+            raise ProcessError("illegal 'count' value: %s" % self.count)
+
+    def close(self):
+        self.count -= 1
+        log.debug("[%d] _CountingCloser.close(): count=%d", id(self),
+                  self.count)
+        if self.count == 0:
+            for objectToClose in self.objectsToClose:
+                objectToClose.close()
+
+
+
+#---- public interface
+
+class Process:
+    """Create a process.
+
+    One can optionally specify the starting working directory, the
+    process environment, and std handles to have the child process
+    inherit (all defaults are the parent's current settings). 'wait' and
+    'kill' method allow for control of the child's termination.
+    """
+    # TODO:
+    #   - Rename this or merge it with ProcessOpen somehow.
+    #
+    if sys.platform.startswith("win"):
+        # .wait() argument constants
+        INFINITE = win32event.INFINITE
+        # .wait() return error codes
+        WAIT_FAILED = win32event.WAIT_FAILED
+        WAIT_TIMEOUT = win32event.WAIT_TIMEOUT
+        # creation "flags" constants
+        # XXX Should drop these and just document usage of
+        #     win32process.CREATE_* constants on windows.
+        CREATE_NEW_CONSOLE = win32process.CREATE_NEW_CONSOLE
+    else:
+        # .wait() argument constants
+        INFINITE = 0
+        # .wait() return error codes
+        WAIT_TIMEOUT = 258
+        WAIT_FAILED = -1
+        # creation "flags" constants
+        CREATE_NEW_CONSOLE = 0x10 # same as win32process.CREATE_NEW_CONSOLE
+
+    def __init__(self, cmd, cwd=None, env=None, flags=0):
+        """Create a child process.
+
+        "cmd" is a command string or argument vector to spawn.
+        "cwd" is a working directory in which to start the child process.
+        "env" is an environment dictionary for the child.
+        "flags" are system-specific process creation flags. On Windows
+            this can be a bitwise-OR of any of the win32process.CREATE_*
+            constants (Note: win32process.CREATE_NEW_PROCESS_GROUP is always
+            OR'd in). On Unix, this is currently ignored.
+        """
+        log.info("Process.__init__(cmd=%r, cwd=%r, env=%r, flags=%r)",
+                 cmd, cwd, env, flags)
+        self._cmd = cmd
+        if not self._cmd:
+            raise ProcessError("You must specify a command.")
+        self._cwd = cwd
+        self._env = env
+        self._flags = flags
+        if sys.platform.startswith("win"):
+            self._flags |= win32process.CREATE_NEW_PROCESS_GROUP
+
+        if sys.platform.startswith("win"):
+            self._startOnWindows()
+        else:
+            self.__retvalCache = None
+            self._startOnUnix()
+
+    def _runChildOnUnix(self):
+        #XXX Errors running the child do *not* get communicated back.
+
+        #XXX Perhaps we should *always* prefix with '/bin/sh -c'? There is a
+        #    disparity btwn how this works on Linux and Windows.
+        if isinstance(self._cmd, types.StringTypes):
+            # This is easier than trying to reproduce shell interpretation to
+            # separate the arguments.
+            cmd = ['/bin/sh', '-c', self._cmd]
+        else:
+            cmd = self._cmd
+
+        # Close all file descriptors (except std*) inherited from the parent.
+        MAXFD = 256 # Max number of file descriptors (os.getdtablesize()???)
+        for i in range(3, MAXFD):
+            try:
+                os.close(i)
+            except OSError:
+                pass
+
+        try:
+            if self._env:
+                os.execvpe(cmd[0], cmd, self._env)
+            else:
+                os.execvp(cmd[0], cmd)
+        finally:
+            os._exit(1)  # Should never get here.
+
+    def _forkAndExecChildOnUnix(self):
+        """Fork and start the child process.
+
+        Sets self._pid as a side effect.
+        """
+        pid = os.fork()
+        if pid == 0: # child
+            self._runChildOnUnix()
+        # parent
+        self._pid = pid
+
+    def _startOnUnix(self):
+        if self._cwd:
+            oldDir = os.getcwd()
+            try:
+                os.chdir(self._cwd)
+            except OSError, ex:
+                raise ProcessError(msg=str(ex), errno=ex.errno)
+        self._forkAndExecChildOnUnix()
+
+        # parent
+        if self._cwd:
+            os.chdir(oldDir)
+
+    def _startOnWindows(self):
+        if type(self._cmd) in (types.ListType, types.TupleType):
+            # And arg vector was passed in.
+            cmd = _joinArgv(self._cmd)
+        else:
+            cmd = self._cmd
+
+        si = win32process.STARTUPINFO() 
+        si.dwFlags = win32process.STARTF_USESHOWWINDOW
+        si.wShowWindow = SW_SHOWDEFAULT
+
+        if not (self._flags & self.CREATE_NEW_CONSOLE):
+            #XXX This is hacky.
+            # We cannot then use _fixupCommand because this will cause a
+            # shell to be openned as the command is launched. Therefore need
+            # to ensure be have the full path to the executable to launch.
+            try:
+                cmd = _whichFirstArg(cmd, self._env)
+            except ProcessError:
+                # Could not find the command, perhaps it is an internal
+                # shell command -- fallback to _fixupCommand
+                cmd = _fixupCommand(cmd, self._env)
+        else:
+            cmd = _fixupCommand(cmd, self._env)
+        log.debug("cmd = %r", cmd)
+
+        # Start the child process.
+        try:
+            self._hProcess, self._hThread, self._processId, self._threadId\
+                = _SaferCreateProcess(
+                    None,           # app name
+                    cmd,            # command line 
+                    None,           # process security attributes 
+                    None,           # primary thread security attributes 
+                    0,              # handles are inherited 
+                    self._flags,    # creation flags 
+                    self._env,      # environment
+                    self._cwd,      # current working directory
+                    si)             # STARTUPINFO pointer 
+            win32api.CloseHandle(self._hThread)
+        except win32api.error, ex:
+            raise ProcessError(msg="Error creating process for '%s': %s"\
+                                   % (cmd, ex.args[2]),
+                               errno=ex.args[0])
+
+    def wait(self, timeout=None): 
+        """Wait for the started process to complete.
+        
+        "timeout" (on Windows) is a floating point number of seconds after
+            which to timeout.  Default is win32event.INFINITE.
+        "timeout" (on Unix) is akin to the os.waitpid() "options" argument
+            (os.WNOHANG may be used to return immediately if the process has
+            not exited). Default is 0, i.e. wait forever.
+
+        If the wait time's out it will raise a ProcessError. Otherwise it
+        will return the child's exit value (on Windows) or the child's exit
+        status excoded as per os.waitpid() (on Linux):
+            "a 16-bit number, whose low byte is the signal number that killed
+            the process, and whose high byte is the exit status (if the
+            signal number is zero); the high bit of the low byte is set if a
+            core file was produced."
+        In the latter case, use the os.W*() methods to interpret the return
+        value.
+        """
+        # XXX Or should returning the exit value be move out to another
+        #     function as on Win32 process control? If so, then should
+        #     perhaps not make WaitForSingleObject semantic transformation.
+        if sys.platform.startswith("win"):
+            if timeout is None:
+                timeout = win32event.INFINITE
+            else:
+                timeout = timeout * 1000.0 # Win32 API's timeout is in millisecs
+
+            rc = win32event.WaitForSingleObject(self._hProcess, timeout)
+            if rc == win32event.WAIT_FAILED:
+                raise ProcessError("'WAIT_FAILED' when waiting for process to "\
+                                   "terminate: %r" % self._cmd, rc)
+            elif rc == win32event.WAIT_TIMEOUT:
+                raise ProcessError("'WAIT_TIMEOUT' when waiting for process to "\
+                                   "terminate: %r" % self._cmd, rc)
+
+            retval = win32process.GetExitCodeProcess(self._hProcess)
+        else:
+            # os.waitpid() will raise:
+            #       OSError: [Errno 10] No child processes
+            # on subsequent .wait() calls. Change these semantics to have
+            # subsequent .wait() calls return the exit status and return
+            # immediately without raising an exception.
+            # (XXX It would require synchronization code to handle the case
+            # of multiple simultaneous .wait() requests, however we can punt
+            # on that because it is moot while Linux still has the problem
+            # for which _ThreadFixer() exists.)
+            if self.__retvalCache is not None:
+                retval = self.__retvalCache
+            else:
+                if timeout is None:
+                    timeout = 0
+                pid, sts = os.waitpid(self._pid, timeout)
+                if pid == self._pid:
+                    self.__retvalCache = retval = sts
+                else:
+                    raise ProcessError("Wait for process timed out.",
+                                       self.WAIT_TIMEOUT)
+        return retval
+
+    def kill(self, exitCode=0, gracePeriod=1.0, sig=None):
+        """Kill process.
+        
+        "exitCode" [deprecated, not supported] (Windows only) is the
+            code the terminated process should exit with.
+        "gracePeriod" (Windows only) is a number of seconds the process is
+            allowed to shutdown with a WM_CLOSE signal before a hard
+            terminate is called.
+        "sig" (Unix only) is the signal to use to kill the process. Defaults
+            to signal.SIGKILL. See os.kill() for more information.
+
+        Windows:
+            Try for an orderly shutdown via WM_CLOSE.  If still running
+            after gracePeriod (1 sec. default), terminate.
+        """
+        if sys.platform.startswith("win"):
+            import win32gui
+            # Send WM_CLOSE to windows in this process group.
+            win32gui.EnumWindows(self._close_, 0)
+
+            # Send Ctrl-Break signal to all processes attached to this
+            # console. This is supposed to trigger shutdown handlers in
+            # each of the processes.
+            try:
+                win32api.GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT,
+                                                  self._processId)
+            except AttributeError:
+                log.warn("The win32api module does not have "\
+                         "GenerateConsoleCtrlEvent(). This may mean that "\
+                         "parts of this process group have NOT been killed.")
+            except win32api.error, ex:
+                if ex.args[0] not in (6, 87):
+                    # Ignore the following:
+                    #   api_error: (87, 'GenerateConsoleCtrlEvent', 'The parameter is incorrect.')
+                    #   api_error: (6, 'GenerateConsoleCtrlEvent', 'The handle is invalid.')
+                    # Get error 6 if there is no console.
+                    raise
+            
+            # Last resort: call TerminateProcess if it has not yet.
+            retval = 0
+            try:
+                self.wait(gracePeriod)
+            except ProcessError, ex:
+                log.info("[%s] Process.kill: calling TerminateProcess", id(self))
+                win32process.TerminateProcess(self._hProcess, -1)
+                win32api.Sleep(100) # wait for resources to be released
+
+        else:
+            if sig is None:
+                sig = signal.SIGKILL
+            try:
+                os.kill(self._pid, sig)
+            except OSError, ex:
+                if ex.errno != 3:
+                    # Ignore:   OSError: [Errno 3] No such process
+                    raise
+
+    def _close_(self, hwnd, dummy):
+        """Callback used by .kill() on Windows.
+
+        EnumWindows callback - sends WM_CLOSE to any window owned by this
+        process.
+        """
+        threadId, processId = win32process.GetWindowThreadProcessId(hwnd)
+        if processId == self._processId:
+            import win32gui
+            win32gui.PostMessage(hwnd, WM_CLOSE, 0, 0)
+
+
+class ProcessOpen(Process):
+    """Create a process and setup pipes to it standard handles.
+
+    This is a super popen3.
+    """
+    # TODO:
+    #   - Share some implementation with Process and ProcessProxy.
+    #
+
+    def __init__(self, cmd, mode='t', cwd=None, env=None):
+        """Create a Process with proxy threads for each std handle.
+
+        "cmd" is the command string or argument vector to run.
+        "mode" (Windows only) specifies whether the pipes used to communicate
+            with the child are openned in text, 't', or binary, 'b', mode.
+            This is ignored on platforms other than Windows. Default is 't'.
+        "cwd" optionally specifies the directory in which the child process
+            should be started. Default is None, a.k.a. inherits the cwd from
+            the parent.
+        "env" is optionally a mapping specifying the environment in which to
+            start the child. Default is None, a.k.a. inherits the environment
+            of the parent.
+        """
+        # Keep a reference to ensure it is around for this object's destruction.
+        self.__log = log
+        log.info("ProcessOpen.__init__(cmd=%r, mode=%r, cwd=%r, env=%r)",
+                 cmd, mode, cwd, env)
+        self._cmd = cmd
+        if not self._cmd:
+            raise ProcessError("You must specify a command.")
+        self._cwd = cwd
+        self._env = env
+        self._mode = mode
+        if self._mode not in ('t', 'b'):
+            raise ProcessError("'mode' must be 't' or 'b'.")
+        self._closed = 0
+
+        if sys.platform.startswith("win"):
+            self._startOnWindows()
+        else:
+            self.__retvalCache = None
+            self._startOnUnix()
+
+        _registerProcess(self)
+
+    def __del__(self):
+        #XXX Should probably not rely upon this.
+        logres.info("[%s] ProcessOpen.__del__()", id(self))
+        self.close()
+        del self.__log # drop reference
+
+    def close(self):
+        if not self._closed:
+            self.__log.info("[%s] ProcessOpen.close()" % id(self))
+
+            # Ensure that all IOBuffer's are closed. If they are not, these
+            # can cause hangs. 
+            try:
+                self.__log.info("[%s] ProcessOpen: closing stdin (%r)."\
+                                % (id(self), self.stdin))
+                self.stdin.close()
+            except AttributeError:
+                # May not have gotten far enough in the __init__ to set
+                # self.stdin, etc.
+                pass
+            try:
+                self.__log.info("[%s] ProcessOpen: closing stdout (%r)."\
+                                % (id(self), self.stdout))
+                self.stdout.close()
+            except AttributeError:
+                # May not have gotten far enough in the __init__ to set
+                # self.stdout, etc.
+                pass
+            try:
+                self.__log.info("[%s] ProcessOpen: closing stderr (%r)."\
+                                % (id(self), self.stderr))
+                self.stderr.close()
+            except AttributeError:
+                # May not have gotten far enough in the __init__ to set
+                # self.stderr, etc.
+                pass
+
+            self._closed = 1
+
+    def _forkAndExecChildOnUnix(self, fdChildStdinRd, fdChildStdoutWr,
+                                fdChildStderrWr):
+        """Fork and start the child process.
+
+        Sets self._pid as a side effect.
+        """
+        pid = os.fork()
+        if pid == 0: # child
+            os.dup2(fdChildStdinRd, 0)
+            os.dup2(fdChildStdoutWr, 1)
+            os.dup2(fdChildStderrWr, 2)
+            self._runChildOnUnix()
+        # parent
+        self._pid = pid
+
+    def _startOnUnix(self):
+        # Create pipes for std handles.
+        fdChildStdinRd, fdChildStdinWr = os.pipe()
+        fdChildStdoutRd, fdChildStdoutWr = os.pipe()
+        fdChildStderrRd, fdChildStderrWr = os.pipe()
+
+        if self._cwd:
+            oldDir = os.getcwd()
+            try:
+                os.chdir(self._cwd)
+            except OSError, ex:
+                raise ProcessError(msg=str(ex), errno=ex.errno)
+        self._forkAndExecChildOnUnix(fdChildStdinRd, fdChildStdoutWr,
+                                     fdChildStderrWr)
+        if self._cwd:
+            os.chdir(oldDir)
+
+        os.close(fdChildStdinRd)
+        os.close(fdChildStdoutWr)
+        os.close(fdChildStderrWr)
+
+        self.stdin = _FileWrapper(descriptor=fdChildStdinWr)
+        logres.info("[%s] ProcessOpen._start(): create child stdin: %r",
+                    id(self), self.stdin)
+        self.stdout = _FileWrapper(descriptor=fdChildStdoutRd)
+        logres.info("[%s] ProcessOpen._start(): create child stdout: %r",
+                    id(self), self.stdout)
+        self.stderr = _FileWrapper(descriptor=fdChildStderrRd)
+        logres.info("[%s] ProcessOpen._start(): create child stderr: %r",
+                    id(self), self.stderr)
+
+    def _startOnWindows(self):
+        if type(self._cmd) in (types.ListType, types.TupleType):
+            # An arg vector was passed in.
+            cmd = _joinArgv(self._cmd)
+        else:
+            cmd = self._cmd
+
+        # Create pipes for std handles.
+        # (Set the bInheritHandle flag so pipe handles are inherited.)
+        saAttr = pywintypes.SECURITY_ATTRIBUTES()
+        saAttr.bInheritHandle = 1
+        #XXX Should maybe try with os.pipe. Dunno what that does for
+        #    inheritability though.
+        hChildStdinRd, hChildStdinWr = win32pipe.CreatePipe(saAttr, 0) 
+        hChildStdoutRd, hChildStdoutWr = win32pipe.CreatePipe(saAttr, 0) 
+        hChildStderrRd, hChildStderrWr = win32pipe.CreatePipe(saAttr, 0) 
+
+        try:
+            # Duplicate the parent ends of the pipes so they are not
+            # inherited. 
+            hChildStdinWrDup = win32api.DuplicateHandle(
+                win32api.GetCurrentProcess(),
+                hChildStdinWr,
+                win32api.GetCurrentProcess(),
+                0,
+                0, # not inherited
+                DUPLICATE_SAME_ACCESS)
+            win32api.CloseHandle(hChildStdinWr)
+            self._hChildStdinWr = hChildStdinWrDup
+            hChildStdoutRdDup = win32api.DuplicateHandle(
+                win32api.GetCurrentProcess(),
+                hChildStdoutRd,
+                win32api.GetCurrentProcess(),
+                0,
+                0, # not inherited
+                DUPLICATE_SAME_ACCESS)
+            win32api.CloseHandle(hChildStdoutRd)
+            self._hChildStdoutRd = hChildStdoutRdDup
+            hChildStderrRdDup = win32api.DuplicateHandle(
+                win32api.GetCurrentProcess(),
+                hChildStderrRd,
+                win32api.GetCurrentProcess(),
+                0,
+                0, # not inherited
+                DUPLICATE_SAME_ACCESS)
+            win32api.CloseHandle(hChildStderrRd)
+            self._hChildStderrRd = hChildStderrRdDup
+
+            # Set the translation mode and buffering.
+            if self._mode == 't':
+                flags = os.O_TEXT
+            else:
+                flags = 0
+            fdChildStdinWr = msvcrt.open_osfhandle(self._hChildStdinWr, flags)
+            fdChildStdoutRd = msvcrt.open_osfhandle(self._hChildStdoutRd, flags)
+            fdChildStderrRd = msvcrt.open_osfhandle(self._hChildStderrRd, flags)
+
+            self.stdin = _FileWrapper(descriptor=fdChildStdinWr,
+                                      handle=self._hChildStdinWr)
+            logres.info("[%s] ProcessOpen._start(): create child stdin: %r",
+                        id(self), self.stdin)
+            self.stdout = _FileWrapper(descriptor=fdChildStdoutRd,
+                                       handle=self._hChildStdoutRd)
+            logres.info("[%s] ProcessOpen._start(): create child stdout: %r",
+                        id(self), self.stdout)
+            self.stderr = _FileWrapper(descriptor=fdChildStderrRd,
+                                       handle=self._hChildStderrRd)
+            logres.info("[%s] ProcessOpen._start(): create child stderr: %r",
+                        id(self), self.stderr)
+
+            # Start the child process.
+            si = win32process.STARTUPINFO() 
+            si.dwFlags = win32process.STARTF_USESHOWWINDOW
+            si.wShowWindow = 0 # SW_HIDE
+            si.hStdInput = hChildStdinRd
+            si.hStdOutput = hChildStdoutWr
+            si.hStdError = hChildStderrWr
+            si.dwFlags |= win32process.STARTF_USESTDHANDLES
+
+            cmd = _fixupCommand(cmd, self._env)
+
+            creationFlags = win32process.CREATE_NEW_PROCESS_GROUP
+            try:
+                self._hProcess, hThread, self._processId, threadId\
+                    = _SaferCreateProcess(
+                        None,           # app name
+                        cmd,            # command line 
+                        None,           # process security attributes 
+                        None,           # primary thread security attributes 
+                        1,              # handles are inherited 
+                        creationFlags,  # creation flags 
+                        self._env,      # environment
+                        self._cwd,      # current working directory
+                        si)             # STARTUPINFO pointer 
+            except win32api.error, ex:
+                raise ProcessError(msg=ex.args[2], errno=ex.args[0])
+            win32api.CloseHandle(hThread)
+
+        finally:
+            # Close child ends of pipes on the parent's side (the
+            # parent's ends of the pipe are closed in the _FileWrappers.)
+            win32file.CloseHandle(hChildStdinRd)
+            win32file.CloseHandle(hChildStdoutWr)
+            win32file.CloseHandle(hChildStderrWr)
+
+    def wait(self, timeout=None): 
+        """Wait for the started process to complete.
+        
+        "timeout" (on Windows) is a floating point number of seconds after
+            which to timeout.  Default is win32event.INFINITE.
+        "timeout" (on Unix) is akin to the os.waitpid() "options" argument
+            (os.WNOHANG may be used to return immediately if the process has
+            not exited). Default is 0, i.e. wait forever.
+
+        If the wait time's out it will raise a ProcessError. Otherwise it
+        will return the child's exit value (on Windows) or the child's exit
+        status excoded as per os.waitpid() (on Linux):
+            "a 16-bit number, whose low byte is the signal number that killed
+            the process, and whose high byte is the exit status (if the
+            signal number is zero); the high bit of the low byte is set if a
+            core file was produced."
+        In the latter case, use the os.W*() methods to interpret the return
+        value.
+        """
+        # XXX Or should returning the exit value be move out to another
+        #    function as on Win32 process control? If so, then should
+        #    perhaps not make WaitForSingleObject semantic
+        #    transformation.
+        # TODO:
+        #   - Need to rationalize the .wait() API for Windows vs. Unix.
+        #     It is a real pain in the current situation.
+        if sys.platform.startswith("win"):
+            if timeout is None:
+                timeout = win32event.INFINITE
+            else:
+                timeout = timeout * 1000.0 # Win32 API's timeout is in millisecs
+
+            #rc = win32event.WaitForSingleObject(self._hProcess, timeout)
+            rc = win32event.WaitForSingleObject(self._hProcess, int(timeout)) # MATT -- Making timeout an integer
+            if rc == win32event.WAIT_FAILED:
+                raise ProcessError("'WAIT_FAILED' when waiting for process to "\
+                                   "terminate: %r" % self._cmd, rc)
+            elif rc == win32event.WAIT_TIMEOUT:
+                raise ProcessError("'WAIT_TIMEOUT' when waiting for process to "\
+                                   "terminate: %r" % self._cmd, rc)
+
+            retval = win32process.GetExitCodeProcess(self._hProcess)
+        else:
+            # os.waitpid() will raise:
+            #       OSError: [Errno 10] No child processes
+            # on subsequent .wait() calls. Change these semantics to have
+            # subsequent .wait() calls return the exit status and return
+            # immediately without raising an exception.
+            # (XXX It would require synchronization code to handle the case
+            # of multiple simultaneous .wait() requests, however we can punt
+            # on that because it is moot while Linux still has the problem
+            # for which _ThreadFixer() exists.)
+            if self.__retvalCache is not None:
+                retval = self.__retvalCache
+            else:
+                if timeout is None:
+                    timeout = 0
+                pid, sts = os.waitpid(self._pid, timeout)
+                if pid == self._pid:
+                    self.__retvalCache = retval = sts
+                else:
+                    raise ProcessError("Wait for process timed out.",
+                                       self.WAIT_TIMEOUT)
+        _unregisterProcess(self)
+        return retval
+
+    def kill(self, exitCode=0, gracePeriod=1.0, sig=None):
+        """Kill process.
+        
+        "exitCode" [deprecated, not supported] (Windows only) is the
+            code the terminated process should exit with.
+        "gracePeriod" (Windows only) is a number of seconds the process is
+            allowed to shutdown with a WM_CLOSE signal before a hard
+            terminate is called.
+        "sig" (Unix only) is the signal to use to kill the process. Defaults
+            to signal.SIGKILL. See os.kill() for more information.
+
+        Windows:
+            Try for an orderly shutdown via WM_CLOSE.  If still running
+            after gracePeriod (1 sec. default), terminate.
+        """
+        if sys.platform.startswith("win"):
+            import win32gui
+            # Send WM_CLOSE to windows in this process group.
+            win32gui.EnumWindows(self._close_, 0)
+
+            # Send Ctrl-Break signal to all processes attached to this
+            # console. This is supposed to trigger shutdown handlers in
+            # each of the processes.
+            try:
+                win32api.GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT,
+                                                  self._processId)
+            except AttributeError:
+                log.warn("The win32api module does not have "\
+                         "GenerateConsoleCtrlEvent(). This may mean that "\
+                         "parts of this process group have NOT been killed.")
+            except win32api.error, ex:
+                if ex.args[0] not in (6, 87):
+                    # Ignore the following:
+                    #   api_error: (87, 'GenerateConsoleCtrlEvent', 'The parameter is incorrect.')
+                    #   api_error: (6, 'GenerateConsoleCtrlEvent', 'The handle is invalid.')
+                    # Get error 6 if there is no console.
+                    raise
+            
+            # Last resort: call TerminateProcess if it has not yet.
+            retval = 0
+            try:
+                self.wait(gracePeriod)
+            except ProcessError, ex:
+                log.info("[%s] Process.kill: calling TerminateProcess", id(self))
+                win32process.TerminateProcess(self._hProcess, -1)
+                win32api.Sleep(100) # wait for resources to be released
+
+        else:
+            if sig is None:
+                sig = signal.SIGKILL
+            try:
+                os.kill(self._pid, sig)
+            except OSError, ex:
+                if ex.errno != 3:
+                    # Ignore:   OSError: [Errno 3] No such process
+                    raise
+
+        _unregisterProcess(self)
+
+    def _close_(self, hwnd, dummy):
+        """Callback used by .kill() on Windows.
+
+        EnumWindows callback - sends WM_CLOSE to any window owned by this
+        process.
+        """
+        threadId, processId = win32process.GetWindowThreadProcessId(hwnd)
+        if processId == self._processId:
+            import win32gui
+            win32gui.PostMessage(hwnd, WM_CLOSE, 0, 0)
+
+
+class ProcessProxy(Process):
+    """Create a process and proxy communication via the standard handles.
+    """
+    #XXX To add to docstring:
+    #   - stdout/stderr proxy handling
+    #   - stdin proxy handling
+    #   - termination
+    #   - how to .start(), i.e. basic usage rules
+    #   - mention that pased in stdin/stdout/stderr objects have to
+    #     implement at least .write (is .write correct for stdin)?
+    #   - if you pass in stdin, stdout, and/or stderr streams it is the
+    #     user's responsibility to close them afterwards.
+    #   - 'cmd' arg can be a command string or an arg vector
+    #   - etc.
+    #TODO:
+    #   - .suspend() and .resume()? See Win32::Process Perl module.
+    #
+    def __init__(self, cmd, mode='t', cwd=None, env=None,
+                 stdin=None, stdout=None, stderr=None):
+        """Create a Process with proxy threads for each std handle.
+
+        "cmd" is the command string or argument vector to run.
+        "mode" (Windows only) specifies whether the pipes used to communicate
+            with the child are openned in text, 't', or binary, 'b', mode.
+            This is ignored on platforms other than Windows. Default is 't'.
+        "cwd" optionally specifies the directory in which the child process
+            should be started. Default is None, a.k.a. inherits the cwd from
+            the parent.
+        "env" is optionally a mapping specifying the environment in which to
+            start the child. Default is None, a.k.a. inherits the environment
+            of the parent.
+        "stdin", "stdout", "stderr" can be used to specify objects with
+            file-like interfaces to handle read (stdout/stderr) and write
+            (stdin) events from the child. By default a process.IOBuffer
+            instance is assigned to each handler. IOBuffer may be
+            sub-classed. See the IOBuffer doc string for more information.
+        """
+        # Keep a reference to ensure it is around for this object's destruction.
+        self.__log = log
+        log.info("ProcessProxy.__init__(cmd=%r, mode=%r, cwd=%r, env=%r, "\
+                 "stdin=%r, stdout=%r, stderr=%r)",
+                 cmd, mode, cwd, env, stdin, stdout, stderr)
+        self._cmd = cmd
+        if not self._cmd:
+            raise ProcessError("You must specify a command.")
+        self._mode = mode
+        if self._mode not in ('t', 'b'):
+            raise ProcessError("'mode' must be 't' or 'b'.")
+        self._cwd = cwd
+        self._env = env
+        if stdin is None:
+            self.stdin = IOBuffer(name='<stdin>')
+        else:
+            self.stdin = stdin
+        if stdout is None:
+            self.stdout = IOBuffer(name='<stdout>')
+        else:
+            self.stdout = stdout
+        if stderr is None:
+            self.stderr = IOBuffer(name='<stderr>')
+        else:
+            self.stderr = stderr
+        self._closed = 0
+
+        if sys.platform.startswith("win"):
+            self._startOnWindows()
+        else:
+            self.__retvalCache = None
+            self._startOnUnix()
+
+        _registerProcess(self)
+
+    def __del__(self):
+        #XXX Should probably not rely upon this.
+        logres.info("[%s] ProcessProxy.__del__()", id(self))
+        self.close()
+        del self.__log # drop reference
+
+    def close(self):
+        if not self._closed:
+            self.__log.info("[%s] ProcessProxy.close()" % id(self))
+
+            # Ensure that all IOBuffer's are closed. If they are not, these
+            # can cause hangs. 
+            self.__log.info("[%s] ProcessProxy: closing stdin (%r)."\
+                            % (id(self), self.stdin))
+            try:
+                self.stdin.close()
+                self._stdinProxy.join()
+            except AttributeError:
+                # May not have gotten far enough in the __init__ to set
+                # self.stdin, etc.
+                pass
+            self.__log.info("[%s] ProcessProxy: closing stdout (%r)."\
+                            % (id(self), self.stdout))
+            try:
+                self.stdout.close()
+                if self._stdoutProxy is not threading.currentThread():
+                    self._stdoutProxy.join()
+            except AttributeError:
+                # May not have gotten far enough in the __init__ to set
+                # self.stdout, etc.
+                pass
+            self.__log.info("[%s] ProcessProxy: closing stderr (%r)."\
+                            % (id(self), self.stderr))
+            try:
+                self.stderr.close()
+                if self._stderrProxy is not threading.currentThread():
+                    self._stderrProxy.join()
+            except AttributeError:
+                # May not have gotten far enough in the __init__ to set
+                # self.stderr, etc.
+                pass
+
+            self._closed = 1
+
+    def _forkAndExecChildOnUnix(self, fdChildStdinRd, fdChildStdoutWr,
+                                fdChildStderrWr):
+        """Fork and start the child process.
+
+        Sets self._pid as a side effect.
+        """
+        pid = os.fork()
+        if pid == 0: # child
+            os.dup2(fdChildStdinRd, 0)
+            os.dup2(fdChildStdoutWr, 1)
+            os.dup2(fdChildStderrWr, 2)
+            self._runChildOnUnix()
+        # parent
+        self._pid = pid
+
+    def _startOnUnix(self):
+        # Create pipes for std handles.
+        fdChildStdinRd, fdChildStdinWr = os.pipe()
+        fdChildStdoutRd, fdChildStdoutWr = os.pipe()
+        fdChildStderrRd, fdChildStderrWr = os.pipe()
+
+        if self._cwd:
+            oldDir = os.getcwd()
+            try:
+                os.chdir(self._cwd)
+            except OSError, ex:
+                raise ProcessError(msg=str(ex), errno=ex.errno)
+        self._forkAndExecChildOnUnix(fdChildStdinRd, fdChildStdoutWr,
+                                     fdChildStderrWr)
+        if self._cwd:
+            os.chdir(oldDir)
+
+        os.close(fdChildStdinRd)
+        os.close(fdChildStdoutWr)
+        os.close(fdChildStderrWr)
+
+        childStdin = _FileWrapper(descriptor=fdChildStdinWr)
+        logres.info("[%s] ProcessProxy._start(): create child stdin: %r",
+                    id(self), childStdin)
+        childStdout = _FileWrapper(descriptor=fdChildStdoutRd)
+        logres.info("[%s] ProcessProxy._start(): create child stdout: %r",
+                    id(self), childStdout)
+        childStderr = _FileWrapper(descriptor=fdChildStderrRd)
+        logres.info("[%s] ProcessProxy._start(): create child stderr: %r",
+                    id(self), childStderr)
+
+        # Create proxy threads for the out pipes.
+        self._stdinProxy = _InFileProxy(self.stdin, childStdin, name='<stdin>')
+        self._stdinProxy.start()
+        # Clean up the parent's side of <stdin> when it is observed that
+        # the child has closed its side of <stdout> and <stderr>. (This
+        # is one way of determining when it is appropriate to clean up
+        # this pipe, with compromises. See the discussion at the top of
+        # this module.)
+        closer = _CountingCloser([self.stdin, childStdin, self], 2)
+        self._stdoutProxy = _OutFileProxy(childStdout, self.stdout, 
+                                          [closer],
+                                          name='<stdout>')
+        self._stdoutProxy.start()
+        self._stderrProxy = _OutFileProxy(childStderr, self.stderr,
+                                          [closer],
+                                          name='<stderr>')
+        self._stderrProxy.start()
+
+    def _startOnWindows(self):
+        if type(self._cmd) in (types.ListType, types.TupleType):
+            # An arg vector was passed in.
+            cmd = _joinArgv(self._cmd)
+        else:
+            cmd = self._cmd
+
+        # Create pipes for std handles.
+        # (Set the bInheritHandle flag so pipe handles are inherited.)
+        saAttr = pywintypes.SECURITY_ATTRIBUTES()
+        saAttr.bInheritHandle = 1
+        #XXX Should maybe try with os.pipe. Dunno what that does for
+        #    inheritability though.
+        hChildStdinRd, hChildStdinWr = win32pipe.CreatePipe(saAttr, 0) 
+        hChildStdoutRd, hChildStdoutWr = win32pipe.CreatePipe(saAttr, 0) 
+        hChildStderrRd, hChildStderrWr = win32pipe.CreatePipe(saAttr, 0) 
+
+        try:
+            # Duplicate the parent ends of the pipes so they are not
+            # inherited. 
+            hChildStdinWrDup = win32api.DuplicateHandle(
+                win32api.GetCurrentProcess(),
+                hChildStdinWr,
+                win32api.GetCurrentProcess(),
+                0,
+                0, # not inherited
+                DUPLICATE_SAME_ACCESS)
+            win32api.CloseHandle(hChildStdinWr)
+            self._hChildStdinWr = hChildStdinWrDup
+            hChildStdoutRdDup = win32api.DuplicateHandle(
+                win32api.GetCurrentProcess(),
+                hChildStdoutRd,
+                win32api.GetCurrentProcess(),
+                0,
+                0, # not inherited
+                DUPLICATE_SAME_ACCESS)
+            win32api.CloseHandle(hChildStdoutRd)
+            self._hChildStdoutRd = hChildStdoutRdDup
+            hChildStderrRdDup = win32api.DuplicateHandle(
+                win32api.GetCurrentProcess(),
+                hChildStderrRd,
+                win32api.GetCurrentProcess(),
+                0,
+                0, # not inherited
+                DUPLICATE_SAME_ACCESS)
+            win32api.CloseHandle(hChildStderrRd)
+            self._hChildStderrRd = hChildStderrRdDup
+
+            # Set the translation mode.
+            if self._mode == 't':
+                flags = os.O_TEXT
+                mode = ''
+            else:
+                flags = 0
+                mode = 'b'
+            fdChildStdinWr = msvcrt.open_osfhandle(self._hChildStdinWr, flags)
+            fdChildStdoutRd = msvcrt.open_osfhandle(self._hChildStdoutRd, flags)
+            fdChildStderrRd = msvcrt.open_osfhandle(self._hChildStderrRd, flags)
+
+            childStdin = _FileWrapper(descriptor=fdChildStdinWr,
+                                      handle=self._hChildStdinWr)
+            logres.info("[%s] ProcessProxy._start(): create child stdin: %r",
+                        id(self), childStdin)
+            childStdout = _FileWrapper(descriptor=fdChildStdoutRd,
+                                       handle=self._hChildStdoutRd)
+            logres.info("[%s] ProcessProxy._start(): create child stdout: %r",
+                        id(self), childStdout)
+            childStderr = _FileWrapper(descriptor=fdChildStderrRd,
+                                       handle=self._hChildStderrRd)
+            logres.info("[%s] ProcessProxy._start(): create child stderr: %r",
+                        id(self), childStderr)
+
+            # Start the child process.
+            si = win32process.STARTUPINFO() 
+            si.dwFlags = win32process.STARTF_USESHOWWINDOW
+            si.wShowWindow = 0 # SW_HIDE
+            si.hStdInput = hChildStdinRd
+            si.hStdOutput = hChildStdoutWr
+            si.hStdError = hChildStderrWr
+            si.dwFlags |= win32process.STARTF_USESTDHANDLES
+
+            cmd = _fixupCommand(cmd, self._env)
+            log.debug("cmd = %r", cmd)
+
+            creationFlags = win32process.CREATE_NEW_PROCESS_GROUP
+            try:
+                self._hProcess, hThread, self._processId, threadId\
+                    = _SaferCreateProcess(
+                        None,           # app name
+                        cmd,            # command line 
+                        None,           # process security attributes 
+                        None,           # primary thread security attributes 
+                        1,              # handles are inherited 
+                        creationFlags,  # creation flags 
+                        self._env,      # environment
+                        self._cwd,      # current working directory
+                        si)             # STARTUPINFO pointer 
+            except win32api.error, ex:
+                raise ProcessError(msg=ex.args[2], errno=ex.args[0])
+            win32api.CloseHandle(hThread)
+
+        finally:
+            # Close child ends of pipes on the parent's side (the
+            # parent's ends of the pipe are closed in the _FileWrappers.)
+            win32file.CloseHandle(hChildStdinRd)
+            win32file.CloseHandle(hChildStdoutWr)
+            win32file.CloseHandle(hChildStderrWr)
+
+        # Create proxy threads for the pipes.
+        self._stdinProxy = _InFileProxy(self.stdin, childStdin, name='<stdin>')
+        self._stdinProxy.start()
+        # Clean up the parent's side of <stdin> when it is observed that
+        # the child has closed its side of <stdout>. (This is one way of
+        # determining when it is appropriate to clean up this pipe, with
+        # compromises. See the discussion at the top of this module.)
+        self._stdoutProxy = _OutFileProxy(childStdout, self.stdout, 
+                                          [self.stdin, childStdin, self],
+                                          name='<stdout>')
+        self._stdoutProxy.start()
+        self._stderrProxy = _OutFileProxy(childStderr, self.stderr,
+                                          name='<stderr>')
+        self._stderrProxy.start()
+
+    def wait(self, timeout=None): 
+        """Wait for the started process to complete.
+        
+        "timeout" (on Windows) is a floating point number of seconds after
+            which to timeout.  Default is win32event.INFINITE.
+        "timeout" (on Unix) is akin to the os.waitpid() "options" argument
+            (os.WNOHANG may be used to return immediately if the process has
+            not exited). Default is 0, i.e. wait forever.
+
+        If the wait time's out it will raise a ProcessError. Otherwise it
+        will return the child's exit value (on Windows) or the child's exit
+        status excoded as per os.waitpid() (on Linux):
+            "a 16-bit number, whose low byte is the signal number that killed
+            the process, and whose high byte is the exit status (if the
+            signal number is zero); the high bit of the low byte is set if a
+            core file was produced."
+        In the latter case, use the os.W*() methods to interpret the return
+        value.
+        """
+        # XXX Or should returning the exit value be move out to another
+        #     function as on Win32 process control? If so, then should
+        #     perhaps not make WaitForSingleObject semantic transformation.
+        if sys.platform.startswith("win"):
+            if timeout is None:
+                timeout = win32event.INFINITE
+            else:
+                timeout = timeout * 1000.0 # Win32 API's timeout is in millisecs
+
+            rc = win32event.WaitForSingleObject(self._hProcess, timeout)
+            if rc == win32event.WAIT_FAILED:
+                raise ProcessError("'WAIT_FAILED' when waiting for process to "\
+                                   "terminate: %r" % self._cmd, rc)
+            elif rc == win32event.WAIT_TIMEOUT:
+                raise ProcessError("'WAIT_TIMEOUT' when waiting for process to "\
+                                   "terminate: %r" % self._cmd, rc)
+
+            retval = win32process.GetExitCodeProcess(self._hProcess)
+        else:
+            # os.waitpid() will raise:
+            #       OSError: [Errno 10] No child processes
+            # on subsequent .wait() calls. Change these semantics to have
+            # subsequent .wait() calls return the exit status and return
+            # immediately without raising an exception.
+            # (XXX It would require synchronization code to handle the case
+            # of multiple simultaneous .wait() requests, however we can punt
+            # on that because it is moot while Linux still has the problem
+            # for which _ThreadFixer() exists.)
+            if self.__retvalCache is not None:
+                retval = self.__retvalCache
+            else:
+                if timeout is None:
+                    timeout = 0
+                pid, sts = os.waitpid(self._pid, timeout)
+                if pid == self._pid:
+                    self.__retvalCache = retval = sts
+                else:
+                    raise ProcessError("Wait for process timed out.",
+                                       self.WAIT_TIMEOUT)
+        _unregisterProcess(self)
+        return retval
+
+    def kill(self, exitCode=0, gracePeriod=1.0, sig=None):
+        """Kill process.
+        
+        "exitCode" [deprecated, not supported] (Windows only) is the
+            code the terminated process should exit with.
+        "gracePeriod" (Windows only) is a number of seconds the process is
+            allowed to shutdown with a WM_CLOSE signal before a hard
+            terminate is called.
+        "sig" (Unix only) is the signal to use to kill the process. Defaults
+            to signal.SIGKILL. See os.kill() for more information.
+
+        Windows:
+            Try for an orderly shutdown via WM_CLOSE.  If still running
+            after gracePeriod (1 sec. default), terminate.
+        """
+        if sys.platform.startswith("win"):
+            import win32gui
+            # Send WM_CLOSE to windows in this process group.
+            win32gui.EnumWindows(self._close_, 0)
+
+            # Send Ctrl-Break signal to all processes attached to this
+            # console. This is supposed to trigger shutdown handlers in
+            # each of the processes.
+            try:
+                win32api.GenerateConsoleCtrlEvent(CTRL_BREAK_EVENT,
+                                                  self._processId)
+            except AttributeError:
+                log.warn("The win32api module does not have "\
+                         "GenerateConsoleCtrlEvent(). This may mean that "\
+                         "parts of this process group have NOT been killed.")
+            except win32api.error, ex:
+                if ex.args[0] not in (6, 87):
+                    # Ignore the following:
+                    #   api_error: (87, 'GenerateConsoleCtrlEvent', 'The parameter is incorrect.')
+                    #   api_error: (6, 'GenerateConsoleCtrlEvent', 'The handle is invalid.')
+                    # Get error 6 if there is no console.
+                    raise
+            
+            # Last resort: call TerminateProcess if it has not yet.
+            retval = 0
+            try:
+                self.wait(gracePeriod)
+            except ProcessError, ex:
+                log.info("[%s] Process.kill: calling TerminateProcess", id(self))
+                win32process.TerminateProcess(self._hProcess, -1)
+                win32api.Sleep(100) # wait for resources to be released
+
+        else:
+            if sig is None:
+                sig = signal.SIGKILL
+            try:
+                os.kill(self._pid, sig)
+            except OSError, ex:
+                if ex.errno != 3:
+                    # Ignore:   OSError: [Errno 3] No such process
+                    raise
+
+        _unregisterProcess(self)
+
+    def _close_(self, hwnd, dummy):
+        """Callback used by .kill() on Windows.
+
+        EnumWindows callback - sends WM_CLOSE to any window owned by this
+        process.
+        """
+        threadId, processId = win32process.GetWindowThreadProcessId(hwnd)
+        if processId == self._processId:
+            import win32gui
+            win32gui.PostMessage(hwnd, WM_CLOSE, 0, 0)
+
+
+class IOBuffer:
+    """Want to be able to both read and write to this buffer from
+    difference threads and have the same read/write semantics as for a
+    std handler.
+
+    This class is subclass-able. _doRead(), _doWrite(), _doReadline(),
+    _doClose(), _haveLine(), and _haveNumBytes() can be overridden for
+    specific functionality. The synchronization issues (block on read
+    until write provides the needed data, termination) are handled for
+    free.
+
+    Cannot support:
+        .seek()     # Because we are managing *two* positions (one each
+        .tell()     #   for reading and writing), these do not make
+                    #   sense.
+    """
+    #TODO:
+    #   - Is performance a problem? This will likely be slower that
+    #     StringIO.StringIO().
+    #
+    def __init__(self, mutex=None, stateChange=None, name=None):
+        """'name' can be set for debugging, it will be used in log messages."""
+        if name is not None:
+            self._name = name
+        else:
+            self._name = id(self)
+        log.info("[%s] IOBuffer.__init__()" % self._name)
+
+        self.__buf = ''
+        # A state change is defined as the buffer being closed or a
+        # write occuring.
+        if mutex is not None:
+            self._mutex = mutex
+        else:
+            self._mutex = threading.Lock()
+        if stateChange is not None:
+            self._stateChange = stateChange
+        else:
+            self._stateChange = threading.Condition()
+        self._closed = 0
+
+    def _doWrite(self, s):
+        self.__buf += s  # Append to buffer.
+
+    def write(self, s):
+        log.info("[%s] IOBuffer.write(s=%r)", self._name, s)
+        # Silently drop writes after the buffer has been close()'d.
+        if self._closed:
+            return
+        # If empty write, close buffer (mimicking behaviour from
+        # koprocess.cpp.)
+        if not s:
+            self.close()
+            return
+
+        self._mutex.acquire()
+        self._doWrite(s)
+        self._stateChange.acquire()
+        self._stateChange.notifyAll()   # Notify of the write().
+        self._stateChange.release()
+        self._mutex.release()
+
+    def writelines(self, list):
+        self.write(''.join(list))
+
+    def _doRead(self, n):
+        """Pop 'n' bytes from the internal buffer and return them."""
+        if n < 0:
+            idx = len(self.__buf)
+        else:
+            idx = min(n, len(self.__buf))
+        retval, self.__buf = self.__buf[:idx], self.__buf[idx:]
+        return retval
+
+    def read(self, n=-1):
+        log.info("[%s] IOBuffer.read(n=%r)" % (self._name, n))
+        log.info("[%s] IOBuffer.read(): wait for data" % self._name)
+        if n < 0:
+            # Wait until the buffer is closed, i.e. no more writes will
+            # come.
+            while 1:
+                if self._closed: break
+                #log.debug("[%s]     <<< IOBuffer.read: state change .wait()"\
+                #          % self._name)
+                self._stateChange.acquire()
+                self._stateChange.wait()
+                self._stateChange.release()
+                #log.debug("[%s]     >>> IOBuffer.read: done change .wait()"\
+                #          % self._name)
+        else:
+            # Wait until there are the requested number of bytes to read
+            # (or until the buffer is closed, i.e. no more writes will
+            # come).
+            # XXX WARNING: I *think* there is a race condition around
+            #     here whereby self.fparent.read() in _InFileProxy can
+            #     hang. *Sometime* test_stdin::test_stdin_buffer() will
+            #     hang. This was *before* I moved the
+            #     _stateChange.acquire() and .release() calls out side
+            #     of the 'while 1:' here. ...and now they are back
+            #     inside.
+            while 1:
+                if self._closed: break
+                if self._haveNumBytes(n): break
+                #log.debug("[%s]     <<< IOBuffer.read: state change .wait()"\
+                #          % self._name)
+                self._stateChange.acquire()
+                self._stateChange.wait()
+                self._stateChange.release()
+                #log.debug("[%s]     >>> IOBuffer.read: done change .wait()"\
+                #          % self._name)
+        log.info("[%s] IOBuffer.read(): done waiting for data" % self._name)
+
+        self._mutex.acquire()
+        retval = self._doRead(n)
+        self._mutex.release()
+        return retval
+
+    def _doReadline(self, n):
+        """Pop the front line (or n bytes of it, whichever is less) from
+        the internal buffer and return it.
+        """
+        idx = self.__buf.find('\n')
+        if idx == -1:
+            idx = len(self.__buf)
+        else:
+            idx += 1 # include the '\n'
+        if n is not None:
+            idx = min(idx, n) 
+        retval, self.__buf = self.__buf[:idx], self.__buf[idx:]
+        return retval
+
+    def _haveLine(self):
+        return self.__buf.find('\n') != -1
+
+    def _haveNumBytes(self, n=None):
+        return len(self.__buf) >= n
+
+    def readline(self, n=None):
+        # Wait until there is a full line (or at least 'n' bytes)
+        # in the buffer or until the buffer is closed, i.e. no more
+        # writes will come.
+        log.info("[%s] IOBuffer.readline(n=%r)" % (self._name, n))
+
+        log.info("[%s] IOBuffer.readline(): wait for data" % self._name)
+        while 1:
+            if self._closed: break
+            if self._haveLine(): break
+            if n is not None and self._haveNumBytes(n): break
+            self._stateChange.acquire()
+            self._stateChange.wait()
+            self._stateChange.release()
+        log.info("[%s] IOBuffer.readline(): done waiting for data"\
+                 % self._name)
+
+        self._mutex.acquire()
+        retval = self._doReadline(n)
+        self._mutex.release()
+        return retval
+
+    def readlines(self):
+        lines = []
+        while 1:
+            line = self.readline()
+            if line:
+                lines.append(line)
+            else:
+                break
+        return lines
+
+    def _doClose(self):
+        pass
+
+    def close(self):
+        if not self._closed:
+            log.info("[%s] IOBuffer.close()" % self._name)
+            self._doClose()
+            self._closed = 1
+            self._stateChange.acquire()
+            self._stateChange.notifyAll()   # Notify of the close().
+            self._stateChange.release()
+
+    def flush(self):
+        log.info("[%s] IOBuffer.flush()" % self._name)
+        #XXX Perhaps flush() should unwedged possible waiting .read()
+        #    and .readline() calls that are waiting for more data???
+
+
+class _InFileProxy(threading.Thread):
+    """A thread to proxy stdin.write()'s from the parent to the child."""
+    def __init__(self, fParent, fChild, name=None):
+        """
+        "fParent" is a Python file-like object setup for writing.
+        "fChild" is a Win32 handle to the a child process' output pipe.
+        "name" can be set for debugging, it will be used in log messages.
+        """
+        log.info("[%s, %s] _InFileProxy.__init__(fChild=%r, fParent=%r)",
+                 name, id(self), fChild, fParent)
+        threading.Thread.__init__(self, name=name)
+        self.fChild = fChild
+        self.fParent = fParent
+
+    def run(self):
+        log.info("[%s] _InFileProxy: start" % self.getName())
+        try:
+            self._proxyFromParentToChild()
+        finally:
+            log.info("[%s] _InFileProxy: closing parent (%r)"\
+                     % (self.getName(), self.fParent))
+            try:
+                self.fParent.close()
+            except IOError:
+                pass # Ignore: IOError: [Errno 4] Interrupted system call
+        log.info("[%s] _InFileProxy: done" % self.getName())
+
+    def _proxyFromParentToChild(self):
+        CHUNKSIZE = 4096
+        # Read output from the child process, and (for now) just write
+        # it out.
+        while 1:
+            log.info("[%s] _InFileProxy: waiting for read on parent (%r)"\
+                     % (self.getName(), self.fParent))
+            # XXX Get hangs here (!) even with
+            #     self.stdin.close() in ProcessProxy' __del__() under this
+            #     cond:
+            #           p = ProcessProxy([...], stdin=sys.stdin)
+            #     The user must manually send '\n' via <Enter> or EOF
+            #     via <Ctrl-Z> to unlock this. How to get around that?
+            #     See cleanOnTermination note in _OutFileProxy.run()
+            #     below.
+            #log.debug("XXX          -> start read on %r" % self.fParent)
+            try:
+                text = self.fParent.read(CHUNKSIZE)
+            except ValueError, ex:
+                # ValueError is raised with trying to write to a closed
+                # file/pipe.
+                text = None
+            #log.debug("XXX          <- done read on %r" % self.fParent)
+            if not text:
+                # Empty text signifies that the pipe has been closed on
+                # the parent's end.
+                log.info("[%s] _InFileProxy: observed close of parent (%r)"\
+                         % (self.getName(), self.fParent))
+                # Signal the child so it knows to stop listening.
+                try:
+                    logres.info("[%s] _InFileProxy: closing child after "\
+                                "observing parent's close: %r", self.getName(),
+                                self.fChild)
+                    try:
+                        self.fChild.close()
+                    except IOError:
+                        pass # Ignore: IOError: [Errno 4] Interrupted system call
+                except IOError, ex:
+                    # Ignore: IOError: [Errno 9] Bad file descriptor
+                    # XXX Do we *know* we want to do that?
+                    pass
+                break
+            else:
+                log.info("[%s] _InFileProxy: read %d bytes from parent: %r"\
+                         % (self.getName(), len(text), text))
+
+            log.info("[%s, %s] _InFileProxy: writing %r to child (%r)",
+                     self.getName(), id(self), text, self.fChild)
+            try:
+                self.fChild.write(text)
+            except (OSError, IOError), ex:
+                # Ignore errors for now. For example:
+                # - Get this on Win9x when writing multiple lines to "dir":
+                #   OSError: [Errno 32] Broken pipe
+                #XXX There *may* be errors we don't want to avoid.
+                #XXX Should maybe just ignore EnvironmentError (base class).
+                log.info("[%s] _InFileProxy: error writing to child (%r), "\
+                         "closing: %s" % (self.getName(), self.fParent, ex))
+                break
+            log.info("[%s] _InFileProxy: wrote %d bytes to child: %r"\
+                     % (self.getName(), len(text), text))
+
+
+class _OutFileProxy(threading.Thread):
+    """A thread to watch an "out" file from the spawned child process
+    and pass on write's to the parent.
+    """
+    def __init__(self, fChild, fParent, toClose=[], name=None):
+        """
+        "fChild" is a Win32 handle to the a child process' output pipe.
+        "fParent" is a Python file-like object setup for writing.
+        "toClose" is a list of objects on which to call .close when this
+            proxy is terminating.
+        "name" can be set for debugging, it will be used in log messages.
+        """
+        log.info("[%s] _OutFileProxy.__init__(fChild=%r, fParent=%r, "\
+                 "toClose=%r)", name, fChild, fParent, toClose)
+        threading.Thread.__init__(self, name=name)
+        self.fChild = fChild
+        self.fParent = fParent
+        self.toClose = toClose
+
+    def run(self):
+        log.info("[%s] _OutFileProxy: start" % self.getName())
+        try:
+            self._proxyFromChildToParent()
+        finally:
+            logres.info("[%s] _OutFileProxy: terminating, close child (%r)",
+                        self.getName(), self.fChild)
+            try:
+                self.fChild.close()
+            except IOError:
+                pass # Ignore: IOError: [Errno 4] Interrupted system call
+            log.info("[%s] _OutFileProxy: closing parent (%r)",
+                     self.getName(), self.fParent)
+            try:
+                self.fParent.close()
+            except IOError:
+                pass # Ignore: IOError: [Errno 4] Interrupted system call
+            while self.toClose:
+                logres.info("[%s] _OutFileProxy: closing %r after "\
+                            "closing parent", self.getName(), self.toClose[0])
+                try:
+                    self.toClose[0].close()
+                except IOError:
+                    pass # Ignore: IOError: [Errno 4] Interrupted system call
+                del self.toClose[0]
+        log.info("[%s] _OutFileProxy: done" % self.getName())
+
+    def _proxyFromChildToParent(self):
+        CHUNKSIZE = 4096
+        # Read output from the child process, and (for now) just write
+        # it out.
+        while 1:
+            text = None
+            try:
+                log.info("[%s] _OutFileProxy: waiting for read on child (%r)"\
+                         % (self.getName(), self.fChild))
+                text = self.fChild.read(CHUNKSIZE)
+            except IOError, ex:
+                # Ignore: IOError: [Errno 9] Bad file descriptor
+                # XXX Do we *know* we want to do that?
+                log.info("[%s] _OutFileProxy: error reading from child (%r), "\
+                         "shutting down: %s", self.getName(), self.fChild, ex)
+                break
+            if not text:
+                # Empty text signifies that the pipe has been closed on
+                # the child's end.
+                log.info("[%s] _OutFileProxy: observed close of child (%r)"\
+                         % (self.getName(), self.fChild))
+                break
+
+            log.info("[%s] _OutFileProxy: text(len=%d): %r",
+                     self.getName(), len(text), text)
+            self.fParent.write(text)
+
+
+
+if sys.platform.startswith("linux"):
+    class _ThreadFixer:
+        """Mixin class for various classes in the Process hierarchy to
+        work around the known LinuxThreads bug where one cannot .wait()
+        on a created process from a subthread of the thread that created
+        the process.
+
+        Usage:
+            class ProcessXXX(_ThreadFixer, BrokenProcessXXX):
+                _pclass = BrokenProcessXXX
+
+        Details:
+            Because we must do all real os.wait() calls on the child
+            process from the thread that spawned it, we use a proxy
+            thread whose only responsibility is just that. The proxy
+            thread just starts the child and then immediately wait's for
+            the child to terminate. On termination is stores the exit
+            status (for use by the main thread) and notifies any thread
+            waiting for this termination (possibly the main thread). The
+            overriden .wait() uses this stored exit status and the
+            termination notification to simulate the .wait().
+        """
+        def __init__(self, *args, **kwargs):
+            # Keep a reference to 'log' ensure it is around for this object's
+            # destruction.
+            self.__log = log
+            self.__waiter = None
+            self.__hasTerminated = threading.Condition()
+            self.__terminationResult = None
+            self.__childStarted = threading.Condition()
+            self._pclass.__init__(self, *args, **kwargs)
+
+        def _forkAndExecChildOnUnix(self, *args, **kwargs):
+            """Fork and start the child process do it in a special subthread
+            that will negotiate subsequent .wait()'s.
+
+            Sets self._pid as a side effect.
+            """
+            self.__waiter = threading.Thread(
+                target=self.__launchAndWait, args=args, kwargs=kwargs)
+
+            # Start subthread that will launch child and wait until it
+            # *has* started.
+            self.__childStarted.acquire()
+            self.__waiter.start()
+            self.__childStarted.wait()
+            self.__childStarted.release()
+
+        def __launchAndWait(self, *args, **kwargs):
+            """Launch the given command and wait for it to terminate.
+
+            When the process has terminated then store its exit value
+            and finish.
+            """
+            logfix.info("start child in thread %s",
+                        threading.currentThread().getName())
+
+            # Spawn the child process and notify the main thread of
+            # this.
+            self.__childStarted.acquire()
+            self._pclass._forkAndExecChildOnUnix(self, *args, **kwargs)
+            self.__childStarted.notifyAll()
+            self.__childStarted.release()
+
+            # Wait on the thread and store appropriate results when
+            # finished.
+            try:
+                waitResult = self._pclass.wait(self)
+            except ProcessError, ex:
+                waitResult = ex
+            self.__hasTerminated.acquire()
+            self.__terminationResult = waitResult
+            self.__hasTerminated.notifyAll()
+            self.__hasTerminated.release()
+
+            self.__waiter = None # drop ref that would keep instance alive
+        
+        def wait(self, timeout=None): 
+            # If the process __hasTerminated then return the exit
+            # status. Otherwise simulate the wait as appropriate.
+            # Note:
+            #   - This class is only used on linux so 'timeout' has the
+            #     Unix 'timeout' semantics.
+            self.__hasTerminated.acquire()
+            if self.__terminationResult is None:
+                if timeout == os.WNOHANG:   # Poll.
+                    self.__hasTerminated.wait(0)
+                else:                       # Block until process finishes.
+                    self.__hasTerminated.wait()
+            terminationResult = self.__terminationResult
+            self.__hasTerminated.release()
+
+            if terminationResult is None:
+                # process has not finished yet
+                raise ProcessError("Wait for process timed out.",
+                                   self.WAIT_TIMEOUT)
+            elif isinstance(terminationResult, Exception):
+                # some error waiting for process termination
+                raise terminationResult
+            else:
+                # the process terminated
+                return terminationResult
+
+    _ThreadBrokenProcess = Process
+    class Process(_ThreadFixer, _ThreadBrokenProcess):
+        _pclass = _ThreadBrokenProcess
+
+    _ThreadBrokenProcessOpen = ProcessOpen
+    class ProcessOpen(_ThreadFixer, _ThreadBrokenProcessOpen):
+        _pclass = _ThreadBrokenProcessOpen
+
+    _ThreadBrokenProcessProxy = ProcessProxy
+    class ProcessProxy(_ThreadFixer, _ThreadBrokenProcessProxy):
+        _pclass = _ThreadBrokenProcessProxy
+
+