#!/usr/bin/env python

#   FILE: PipeDream.py -- This file contains a class that simplifies
#         communicating over pipes.
# AUTHOR: W. Michael Petullo, wp0002@drake.edu
#   DATE: 23 SEPT 1997 
#
# 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, signal

# ============================ PipeDream =======================================
# NOTE: By overloading the method Shutdown (), one can ensure the child process
#       is shutdown gracefully.
#
#       One should be careful to catch run-time errors.  The exception handling
#       mechanism in a main block of code should call PipeDream's Shutdown ()
#       method upon a run-time or syntax error.  This will insure the child 
#       process shuts down gracefully.     
#
#	A little note about the '_' notation on members and methods...
#       According to the "Whole Python FAQ," a "__" at the start of a member
#       or method name causes the member or method to be "mangled."  This
#       causes an effect similar to the private reserved word in C++.  However,
#       the "Python Tutorial" at www.python.org says "Warning: this is an 
#       experimental feature. To avoid all potential problems, refrain from 
#       using identifiers starting with double underscore..."  I have decided
#       to use the '_' notation to let the user know when something is supposed
#       to be private. 
#
#       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.
#
#       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.

SyncError   = "generic.SyncError"
PipeTimeout = "generic.PipeTimeout"

class PipeDream:
# ============================ Constructor =====================================
	def __init__ (self, cmd, args):
	#  PRE: cmd is assigned a path to a command to execute
	#   &&  args is assigned a tuple of arguments to pass to cmd
	# NOTE: The 0ith arg is the name of the program to be executed.
	# POST: cmd has been executed, with pipes open
	#   &&  pipe system has been set up with cmd	
		if (not os.path.exists (cmd)):
			raise IOError
		fdin, fdout, fderr, self._childPID = self._OpenPipes (cmd, args)
		self._pipeIn  = os.fdopen (fdin,  'r')
		self._pipeOut = os.fdopen (fdout, 'w')
		self._pipeErr = os.fdopen (fderr, 'r')
		self._defTimeout = 3   # Default timeout when reading from pipe
		
# ============================ Send () =========================================
	def Send (self, sendstr):
	#  PRE: sendstr is assigned string to write
	# POST: sendstr is written to outgoing pipe
		self._pipeOut.write (sendstr + '\n')
		self._pipeOut.flush ()

# ============================ ReceiveProtocol () ==============================
	def ReceiveProtocol (self, expected):
	#  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)
		
# ============================ ReadLine () ======================================
	def ReadLine (self, timeout = None):
	#  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 necissary because sys.stdin.readline () seems to block
	#       alarms.
		if (timeout == None):
			timeout = self._defTimeout
		
		signal.signal (signal.SIGALRM, self._AlarmHandler)
		signal.alarm (timeout)
		
		l = ''
		while 1:
			c = os.read (self._pipeIn.fileno (), 1)
			if (not c):
				break
			l = l + c
			if (c == '\n'):
				break
		
		signal.alarm (0)
		return (l)

# ============================ ConsumeUpTo () =================================
	def ConsumeUpTo (self, delimChar, num = 1):
	#  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
	# NOTE: This should be used when there is not '\n' in the data you want
	#       to read.  Otherwise use ReadLine.  Final delimChar is consumed.
		signal.signal (signal.SIGALRM, self._AlarmHandler)
		signal.alarm (self._defTimeout)
		
		while (num > 0):
			presentChar = os.read (self._pipeIn.fileno (), 1)
			if (presentChar == delimChar): num = num - 1
			
		signal.alarm (0)
		
# =========================== ChildPID () ======================================
	def ChildPID (self):
	# POST: FCTVAL == _childPID
		return (self._childPID)
		
# =========================== DefTimeout () ====================================
	def DefTimeout (self):
	# POST: FCTVAL == _defTimeout
		return (self._defTimeout)

# ============================ Shutdown () =====================================
	def Shutdown (self):
	# POST: pipes have been closed
	# NOTE: This should be overloaded to insure proper shutdown of child 
	#       process.
		self._pipeIn.close ()
		self._pipeOut.close ()
		self._pipeErr.close ()
		
		# Overload this with an appropriate shutdown
		os.kill (self.pipes._childPid, signal.SIGKILL)

# ============================ _OpenPipes () ===================================
	def _OpenPipes (self, cmd, args):
	#  PRE: cmd is assigned the path to a prolog interpreter
	#   &&  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)
	#       "A better popen2 than popen2.popen2"

		pipe1 = os.pipe ()
		pipe2 = os.pipe ()
		pipe3 = os.pipe ()
		pid = os.fork ()

		if pid:
			# I'm the parent
			os.close (pipe1[1])
			os.close (pipe2[0])
			os.close (pipe3[1])

			return (pipe1[0], pipe2[1], pipe3[0], 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.execv (cmd, args)

# ============================ _AlarmHandler () ================================
	def _AlarmHandler (self, signum, traceback):
	# POST: a PipeTimeout exception is raised
	# NOTE: This is called by alarms on the reading of pipes.
		raise PipeTimeout, traceback
		
# ============================ Destructor ======================================
	def __del__ (self):
	# POST: prolog interpreter is shutdown
	#   &&  this Pylog is destroyed
		self.Shutdown ()
