"""pipedream module

The pipedream module is intended to simplify communicating over pipes.
        
W. Michael Petullo of Flyn Computing (http://www.flyn.org) is the 
original author.  

Bob Green (http://www.speakeasy.net/~bob_green) is the current maintainer of 
the module.  

It was originally included  with W. Michael Petullo's Pylog module.  It is 
now released seperately.  The API remains largely the same as the original.

Changes include:

* Cross-platform support.  For it to work on Windows, the Win32 Extensions
for Python must be installed, as it relies on win32pipe.  The Unix version
relies on the original os.fork code.

* Threaded process reads to an input queue allowing timeouts without using
the unix-specific signal.alarm.  Timeouts are explicitly implemented in the
PipeDream.ReadLine method.

* Unit tests are included (unix-runnable only, so far)

---

Copyright (c) 2003 Robert Green
Copyright (c) 1999 W. Michael Petullo
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:

1. Redistributions of source code must retain the above copyright
   notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
   notice in the documentation and/or other materials provided with
   the distribution.

THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""

import os
import time
import string
import signal
import Queue
import thread
import sys

class WriteError(Exception):
    """Raise if an attempted write to pipes failed"""
    pass

class SyncError(Exception):
    """SyncError is provided as an exception that is raised if 
    it is detected that communication between parent and child has fallen 
    out of sync.  For example, if you do not receive expected an protocol 
    from the child.
    """
    pass

class PipeTimeout(Exception):
    """PipeTimeout is provided as an exception that is raised 
    when a read from the pipe times out.  This means you tried to read 
    from an empty pipe."""
    pass
    
class DependencyError(Exception):
    """Raise if a required dependency is not available"""
    pass
    
class OSError(Exception):
    """Raise if OS does not have necessary functionality"""
    pass
    
class PipeDream:
    """PipeDream is a class meant to simplify communicating over pipes.
    
    A PipeDream instance is initialized with an executable command (full
    path or executable in search path), a list of command-line arguments
    for that command and optionally, a default timeout for reading (in 
    seconds), and a pipesize which is the number of characters to read 
    ahead off the in pipe.
    
    e.g.
    pd = pipedream.PipeDream("cat",["README.txt"])
    
    Notes:
    '_' prefixes are used by convention for private/protected methods and
    instance variables.  PipeDream users should not call/reference these
    directly.
    """

    __slots__ = ['_eof', '_in', '_out', '_err', 
                 '_childPID', '_timeout', '_queue']
        
    def __init__(self, cmd, args=[], timeout=6, pipesize=256):
        """Initialize the class with a system executable command and args
        
        Note the default timeout of 6 seconds is rather long for most
        applications.  If its too long for you, you can either specify
        a shorter default timeout here, when creating PipeDreams, or
        you can specify a call specific timeout when calling ReadLine
        based on your longest expected response time.
        
        PRE: cmd is assigned a path to a command to execute
             &&  args is assigned a list of arguments to pass to cmd
        POST: cmd has been executed, with pipes open
              &&  pipe system has been set up with cmd
        """
        self._eof = 0
        self._pipesize = pipesize
        self._timeout = timeout
        self._queue = Queue.Queue(pipesize)
        (self._in, self._out, self._err, 
         self._childPID) = self._OpenPipes(cmd, args)
        thread.start_new_thread(self._ReadToQueue, ())

    def __del__(self):
        """Cleanup pipes on destruction
        
        POST: this object is destroyed
              && child process is shutdown
        """
        self.Shutdown()

    def _ReadToQueue(self):
        done = 0
        while 1:
            if self._queue.full():
                time.sleep(1)
            else:
                try:
                    c = os.read(self._in.fileno(), 1)
                except (IOError, OSError):
                    done = 1
                if (c == '') or done:
                    while not self._queue.empty():
                        time.sleep(1)
                    self._eof = 1
                    thread.exit()
                else:
                    self._queue.put(c)

    def _OpenPipes(self, cmd, args):
        """A better popen2 than popen2.popen2
        
        PRE: cmd is assigned the command to start the application to be
                 communicated with
             &&  args is assigned a tuple containing any arguments to pass 
                 to cmd
        POST: cmd has been spawned with args as arguments
              &&  FCTVAL = (file descriptor to incoming pipe,
                  file descriptor to outgoing pipe,
                  file descriptor to incoming error pipe,
                  child process's pid)
        """
        
        if sys.platform == "win32":
            try:
                import win32pipe
                s = ""
                for arg in args:
                    s += arg + " "
                pipe2, pipe1, pipe3 = win32pipe.popen3(cmd + " " + string.strip(s))
                return (pipe1, pipe2, pipe3, None)
            
            except ImportError:
                raise DependencyError, "Win32 Extensions not installed"
                
        else:
            pipe1 = os.pipe()
            pipe2 = os.pipe()
            pipe3 = os.pipe()
            try:
                pid = os.fork()
                if pid:
                    # I'm the parent
                    os.close(pipe1[1])
                    os.close(pipe2[0])
                    os.close(pipe3[1])

                    return (os.fdopen(pipe1[0],  'r'),
                            os.fdopen(pipe2[1], 'w'),
                            os.fdopen(pipe3[0], 'r'),
                            pid)
                else:
                    # I'm the child
                    os.close(pipe1[0])
                    os.close(pipe2[1])
                    os.close(pipe3[0])

                    os.dup2(pipe1[1], 1)
                    os.dup2(pipe2[0], 0)
                    os.dup2(pipe3[1], 2)
                    os.execvp(cmd, [cmd] + args)

            except AttributeError:
                raise OSError, "OS does not support fork"
                
    def Send(self, sendstr):
        """Send a string on the input pipe
        
        PRE: sendstr is assigned string to write
        POST: sendstr is written to outgoing pipe
        """
        try:
            self._out.write(sendstr)
            self._out.flush()
        except ValueError:
            raise WriteError, sendstr
        
    def SendLine(self, sendstr):
        """Send a line on the input pipe
        
        Convenience method that adds a carriage return to the specified string
        """
        self.Send(sendstr + "\n")

    def ReceiveProtocol(self, expected):
        """Receive an expected token
        
        PRE: expected is assigned the string that is expected to be read 
             off pipe
        POST: FCTVAL = expected string was found on pipe
              ||  timeout occurs and self._AlarmHandler is called        
        NOTE: This function will read off the pipe until the expected       
              string is found or a PipeTimeout exception occurs.
        """    
        l = self.ReadLine()
        while(not (l == expected + '\n')):
            # Read off the pipe until l is encountered
            try:
                l = self.ReadLine()
            except PipeTimeout:
                # ASSERT: l was not found on the pipe
                raise SyncError, l
        return 1

    def HasDataWaiting(self):
        """Reports whether there is data waiting to be read"""
        return not self._queue.empty()
        
    def ReadLine(self, timeout = None):
        """Return the string of characters up to and including the newline
        
        PRE: timeout is time in seconds to wait to read from pipeIn
        POST: FCTVAL == a line read from pipeIn
              ||  timeout occurs and self._AlarmHandler is called
        NOTE: This is necessary because sys.stdin.readline () seems to 
              block alarms.
        """
        if (timeout == None):
            timeout = self._timeout

        line = ""
        c = ''
        timedout = 0
        while c <> '\n' and not timedout:
            start_time = time.time()
            while self._queue.empty() and not timedout:
                time.sleep(0.001)
                elapsed = (time.time() - start_time)
                timedout = elapsed > timeout
            if timedout:
                raise PipeTimeout, line
            else:
                c = self._queue.get()
                line += c
        return line

    def ConsumeUpTo(self, delimChar, num = 1):
        """Eat up all in pipe data until the specified number of the
        specified character are read.
        
        This should be used when there is not '\n' in the data you 
        want to read.  Otherwise use ReadLine.  Final delimChar is 
        consumed.
        
        PRE: char is assigned char to read up through
             &&  num is assigned num of delimChars to read
        POST: data in self.inPipe has been read up to num occurance of char
        """        
        while (num > 0):
            presentChar = os.read(self._in.fileno(), 1)
            if (presentChar == delimChar): 
                num = num - 1

    def Shutdown(self):
        """Shutdown the pipes, wait for the process to end
        
        This method does not kill the process.  In the case of an 
        interactive process, it is the responsibility of the PipeDream
        user to send the appropriate command to cause it to naturally
        shutdown.
        
        POST: pipes have been closed, process return value has been
        cleared
        """
        self._in.close()
        self._out.close()
        self._err.close()
        if sys.platform <> "win32":
            os.waitpid(self._childPID, 0)

                
    def GetChildPID(self):
        """Return the process id of the child process
        
        POST: FCTVAL == _childPID
        """
        return self._childPID

    def GetDefaultTimeout(self):
        """Return the default timeout value
        
        POST: FCTVAL == _timeout
        """
        return self._timeout
