#!/usr/bin/env python
#############################################################################
#
#  Linux Desktop Testing Project http://ldtp.freedesktop.org
# 
#  Author:
#     A. Nagappan <nagappan@gmail.com>
# 
#  Copyright 2004 - 2007 Novell, Inc.
# 
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU Lesser General Public
#  License as published by the Free Software Foundation; either
#  version 2 of the License, or (at your option) any later version.
# 
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
#  Lesser General Public License for more details.
# 
#  You should have received a copy of the GNU Lesser General Public
#  License along with this program; if not, write to the
#  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
#  Boston, MA 02110, USA.
#
#############################################################################

__author__ = "Nagappan A"
__maintainer__ = "Nagappan A"
__version__ = "0.9.2"

import socket, os, types, sys, struct, traceback, time
import threading, re, atexit
import thread, select, signal
from xml.parsers.expat import ExpatError
from xml.dom.minidom import parse, parseString
from xml.sax import saxutils
import ldtplib.ldtprecorder

class command:
	INVALID       = 0
	STOP          = 1
	WINDOWEXIST   = 2
	GETWINDOWNAME = 3
	GETOBJECTNAME = 4

class error (Exception):
	def __init__ (self, value):
		self.value = value
	def __str__ (self):
		return repr (self.value)

class LdtpExecutionError (Exception):
	def __init__ (self, value):
		self.value = value
	def __str__ (self):
		return repr (self.value)

class ConnectionLost (Exception):
	def __init__ (self, value):
		self.value = value
	def __str__ (self):
		return repr (self.value)

class packet:
	def __init__ (self):
		pass
	def generatenotificationxml (self, windowName, objectName, objectType, eventType,
				     timeElapsed, key = None, data = None, detail1 = None,
				     detail2 = None):
		"""This Function will generate some information to LDTP packet
		INPUT: windowname, objectname, objecttype, eventtype[, key[, data]]

		OUTPUT: returns a string in the LDTP packet (XML lib2) format"""
		_xml = '<?xml version=\"1.0\"?>' ## should this header be there in the packets?
		_xml += '<NOTIFICATION>'
		# Fill action name
		_xml = _xml + '<WINDOWNAME>' + saxutils.escape (windowName) + '</WINDOWNAME>'
		_xml = _xml + '<OBJECTNAME>' + saxutils.escape (objectName) + '</OBJECTNAME>'
		_xml = _xml + '<OBJECTTYPE>' + objectType + '</OBJECTTYPE>'
		_xml = _xml + '<EVENTTYPE>' + eventType + '</EVENTTYPE>'
		_xml = _xml + '<TIMEELAPSED>' + str (timeElapsed) + '</TIMEELAPSED>'
		if key:
			_xml = _xml + '<KEY>' + saxutils.escape (key) + '</KEY>'
		if data:
			_xml = _xml + '<DATA>' + saxutils.escape (data) + '</DATA>'
		if detail1:
			_xml = _xml + '<DETAIL1>' + str (detail1) + '</DETAIL1>'
		if detail2:
			_xml = _xml + '<DETAIL2>' + str (detail2) + '</DETAIL2>'
		_xml += '</NOTIFICATION>'
		return _xml

	def generatekeyboardxml (self, text):
		_xml = '<?xml version=\"1.0\"?>' ## should this header be there in the packets?
		_xml += '<KEYBOARD>'
		# Fill action name
		_xml = _xml + '<DATA>' + saxutils.escape (text) + '</DATA>'
		_xml += '</KEYBOARD>'
		return _xml

	def generateresponsexml (self, requestId, status, data = None):
		"""This Function will generate some information to LDTP packet
		INPUT: windowname, objectname, objecttype, eventtype[, key[, data]]

		OUTPUT: returns a string in the LDTP packet (XML lib2) format"""
	
		_xml = '<?xml version=\"1.0\"?>' ## should this header be there in the packets?
		_xml += '<RESPONSE>'
		# Fill action name
		_xml = _xml + '<ID>' + requestId + '</ID>'
		_xml = _xml + '<STATUS><CODE>' + str (status [0]) + '</CODE>'
		if status [0] != 0:
			_xml = _xml + '<MESSAGE>' + status [1] + '</MESSAGE>'
		_xml = _xml + '</STATUS>'
		if data:
			_xml = _xml + '<DATA>' + saxutils.escape (data) + '</DATA>'
		_xml += '</RESPONSE>'
		return _xml

	def getText (self, nodelist):
		rc = ""
		for node in nodelist:
			if node.nodeType == node.TEXT_NODE:
				rc = rc + node.data
		return rc

	def getCData (self, nodelist):
		rc = ""
		for node in nodelist:
			if node.nodeType == node.CDATA_SECTION_NODE:
				rc = rc + node.data
		return rc

	def parsexml (self, xmlpacket):
		"""Returns the value obtained from the server's return LDTP packet"""
		global ldtpDebug
		_requestCommand = 0
		_name           = None
		_requestId      = None
		_requestObj     = None

		try:
			dom = parseString (xmlpacket)
			try:
				_requestObj  = dom.getElementsByTagName ('REQUEST')[0]
			except IndexError:
				if ldtpDebug:
					print 'Invalid request'
				return None
			try:
				_requestCommand = int (self.getText (_requestObj.getElementsByTagName ('COMMAND') [0].childNodes))
			except ValueError:
				if ldtpDebug:
					print 'Invalid Command'
				return None
			except IndexError:
				if ldtpDebug:
					print 'Invalid Command'
				return None
			try:
				_name = self.getText (_requestObj.getElementsByTagName ('NAME') [0].childNodes)
			except ValueError:
				pass
			except IndexError:
				# Data tag may not be present
				pass
			try:
				_requestId  = self.getText (_requestObj.getElementsByTagName ('ID') [0].childNodes)
			except IndexError:
				# On notification _requestId will be empty
				pass
		except ExpatError, msg:
			if msg.code == xml.parsers.expat.errors.XML_ERROR_NO_ELEMENTS:
				return None
			if xml.parsers.expat.ErrorString (msg.code) == xml.parsers.expat.errors.XML_ERROR_NO_ELEMENTS:
				return None
		# Return all the respective values, let the calling function decide what to do with the values
		return _requestId, _requestCommand, _name

	# Send given packet to server
	def sendpacket (self, msg):
		global ldtpDebug, clientFd
		if clientFd is None:
			return
		flag = False
		try:
			timedelay = os.getenv ('LDTP_DELAY_CMD')
			sendLck.acquire ()
			if timedelay != None:
				try:
					# Delay each command by 'timedelay' seconds
					time.sleep (int (timedelay))
				except IndexError:
					# Default to 5 seconds delay if LDTP_DELAY_CMD
					# env variable is set
					time.sleep (5)
			flag = True
			# Encode the message in UTF-8 so we don't break on extended
			# characters in the application GUIs
			buf = msg.encode ('utf8')

			_length =  len (buf)
			# Pack length (integer value) in network byte order
			format = '!i%ds' % _length
			_msg = struct.pack (format, _length, buf)
			# Send message
			clientFd.send (_msg)
			ldtpDebug = os.getenv ('LDTP_DEBUG')
			if ldtpDebug != None and ldtpDebug == '2':
				print 'Send packet', buf
			sendLck.release ()
		except socket.error, msg:
			if flag == True:
				# Reason for using the flag:
				# 'Do not call this method when the lock is unlocked.'
				sendLck.release ()
			#raise LdtpExecutionError ('Client aborted')
			return

	def recvpacket (self, sockfd = None):
		global ldtpDebug, clientFd
		flag = False
		try:
			recvLck.acquire ()
			flag = True
			client = None
			# Get client socket fd based on thread id
			if sockfd == None:
				client = clientFd
			else:
				client = sockfd
			_responsePacket = None
			client.settimeout (5.0)
			# Hardcoded 4 bytes, as the server sends 4 bytes as packet length
			data = client.recv (4, 0)
			if data == '' or data == None:
				if flag == True:
					# Reason for using the flag:
					# 'Do not call this method when the lock is unlocked.'
					recvLck.release ()
				return None
			_packetSize, = struct.unpack('!i', data)
			if ldtpDebug != None and ldtpDebug == '2':
				print 'Received packet size', _packetSize
			# MSG_PEEK
			# This flag causes the receive operation to return data from the beginning
			# of the receive queue without removing that data from  the  queue.
			# Thus, a subsequent receive call will return the same data.

			_responsePacket = client.recv (_packetSize, 0)
			_pattern = re.compile ('\<\?xml')
			_searchObj = re.search (_pattern, _responsePacket)
			_finalPacket = _responsePacket[_searchObj.start () :]
			_responsePacket = _finalPacket
			recvLck.release ()
			if ldtpDebug != None and ldtpDebug == '2':
				print 'Received response Packet', _responsePacket
			return _responsePacket
		except struct.error, msg:
			if flag == True:
				# Reason for using the flag:
				# 'Do not call this method when the lock is unlocked.'
				recvLck.release ()
			raise LdtpExecutionError ('Invalid packet length ' + str (msg))
		except AttributeError, msg:
			if flag == True:
				# Reason for using the flag:
				# 'Do not call this method when the lock is unlocked.'
				recvLck.release ()
			raise LdtpExecutionError ('Error while receiving packet ' + str (msg))
		except socket.timeout:
			if flag == True:
				# Reason for using the flag:
				# 'Do not call this method when the lock is unlocked.'
				recvLck.release ()
			if ldtpDebug != None and ldtpDebug == '2':
				print 'Timeout'
			return ''
		except socket.error, msg:
			if flag == True:
				# Reason for using the flag:
				# 'Do not call this method when the lock is unlocked.'
				recvLck.release ()
			raise LdtpExecutionError ('Error while receiving packet ' + str (msg))
		except MemoryError, msg:
			if flag == True:
				# Reason for using the flag:
				# 'Do not call this method when the lock is unlocked.'
				recvLck.release ()
			raise LdtpExecutionError ('Error while receiving packet ' + str (msg))
		except:
			if flag == True:
				# Reason for using the flag:
				# 'Do not call this method when the lock is unlocked.'
				recvLck.release ()
			raise LdtpExecutionError ('Error while receiving packet')

class PollServer (threading.Thread):
	def __init__ (self):
		threading.Thread.__init__ (self)
		self._events = None
	def run (self):
		try:
			self.start_polling_server ()
		except:
			try:
				raise LdtpExecutionError (str (traceback.format_exc ()))
			except AttributeError:
				pass
			except:
				pass
	def start_polling_server (self):
		self._serverDisconnected = False
		global _serverpoll, mainsock, clientFd
		_serverpoll = select.poll ()
		_serverpoll.register (mainsock, select.POLLIN)

		while True:
			try:
				self._events = _serverpoll.poll ()
			except socket.error, msg:
				break
			except:
				_serverpoll.unregister (mainsock)
				mainsock = None
				sys.exit ()
			try:
				if self._events == None:
					break
				for i in range (0, len (self._events)):
					if (self._events [i][1] & select.POLLIN or self._events [i][1] & select.POLLPRI):
						fd = socket.fromfd (self._events[i][0],
								    socket.AF_UNIX,
								    socket.SOCK_STREAM)
						if self._events [i][0] == mainsock.fileno ():
							clientFd, addr = mainsock.accept ()
							_serverpoll.register (clientFd, select.POLLIN)
							continue
						try:
							if self.handlePacket () == None:
								_serverpoll.unregister (self._events[i][0])
								continue
						except LdtpExecutionError, msg:
							self._serverDisconnected = True
							break
						except SystemExit:
							break
						except:
							print traceback.format_exc ()
					elif (self._events[i][1] & select.POLLNVAL):
						# Unregister newly created socket from polling once its completed
						_serverpoll.unregister (self._events[i][0])
					else:
						self._serverDisconnected = True
						break
				if self._serverDisconnected == True:
					break
			# Checking this exception due to bug # 333090 comment # 6
			except TypeError:
				_serverpoll.unregister (mainsock)
				sys.exit ()
	def handlePacket (self):
		global pckt, recEngine, ldtpDebug
		try:
			self._packet = pckt.recvpacket ()
			if ldtpDebug:
				print self._packet
		except KeyboardInterrupt:
			return None
		except LdtpExecutionError:
			return ''
		try:
			if self._packet == None:
				return None
			if self._packet != '':
				_requestId, _commandCode, _name = pckt.parsexml (self._packet)
				if _commandCode == command.INVALID:
					return ''
				if _commandCode == command.STOP:
					_status = {}
					_status [0] = 0
					_status [1] = 'Success'
					_xml = pckt.generateresponsexml (_requestId, _status)
					pckt.sendpacket (_xml)
					stop ()
					thread.exit ()
				if _commandCode == command.WINDOWEXIST:
					_status = {}
					_status [0] = 0
					_status [1] = 'Success'
					_xml = ''
					if ldtplib.ldtprecorder.recorder.doesWindowExist (_name):
						_xml = pckt.generateresponsexml (_requestId, _status)
					else:
						_status [0] = 1
						_status [1] = 'Window does not exist'
						_xml = pckt.generateresponsexml (_requestId, _status)
					pckt.sendpacket (_xml)
					return ''
				if _commandCode == command.GETWINDOWNAME:
					if ldtpDebug:
						print 'get window name'
					return ''
				if _commandCode == command.GETOBJECTNAME:
					if ldtpDebug:
						print 'get object name'
					_status = {}
					_status [0] = 0
					_status [1] = 'Success'
					_xml = ''
					_objName = ldtplib.ldtprecorder.recorder.getObjectName (_name)
					if _objName != '':
						_xml = pckt.generateresponsexml (_requestId, _status, _objName)
					else:
						_status [0] = 1
						_status [1] = 'Object does not exist'
						_xml = pckt.generateresponsexml (_requestId, _status)
					pckt.sendpacket (_xml)
					return ''
		except TypeError:
			return ''
		except LdtpExecutionError:
			return ''
		except KeyboardInterrupt:
			return None
		return ''
	def shutdown (self):
		global _serverpoll, mainsock
		if mainsock:
			mainsock.close ()
			mainsock = None

class recorder:
	def __init__ (self):
		self.ldtpusetcp = False
		self.ldtpserveraddr = None
		self.ldtpserverport = None
		self.socketpath = None
		self.display = os.getenv ('DISPLAY')
		if self.display == None:
			raise LdtpExecutionError ('Missing DISPLAY environment variable. Running in text mode ?')	
	def start (self):
		self.socketpath = '/tmp/ldtp-record-' + os.getenv ('USER') + '-' + self.display

		if os.environ.has_key("LDTP_SERVER_ADDR"):
			self.ldtpserveraddr = os.environ ["LDTP_SERVER_ADDR"]
			if os.environ.has_key ("LDTP_SERVER_PORT"):
				self.ldtpserverport = int (os.environ["LDTP_SERVER_PORT"])
			else:
				self.ldtpserverport = 23457
			self.ldtpusetcp = True
		try:
			# Create a client socket
			mainsock = None
			if self.ldtpusetcp:
				mainsock = socket.socket (socket.AF_INET, socket.SOCK_STREAM)
			else:
				mainsock = socket.socket (socket.AF_UNIX, socket.SOCK_STREAM)
		except socket.error,msg:
			if self.ldtpusetcp:
				raise LdtpExecutionError ('Error while creating socket  ' + str (msg))
			else:
				raise LdtpExecutionError ('Error while creating UNIX socket  ' + str (msg))	

		try:
			mainsock.setsockopt (socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
			# Connect to server socket
			if self.ldtpusetcp:
				mainsock.bind ((self.ldtpserveraddr, self.ldtpserverport))
			else:
				mainsock.bind (self.socketpath)
			mainsock.listen (5)
			return mainsock
		except TypeError:
			raise LdtpExecutionError ('Environment LDTP_AUTH_SOCK variable not set')
		except socket.error, msg:
			raise LdtpExecutionError ('Recording server could not be start ' + str (msg))
	def stop (self):
		global mainsock
		if mainsock is not None:
			mainsock.close ()
			mainsock = None
		if self.ldtpusetcp is False:
			try:
				os.unlink (self.socketpath)
			except OSError:
				pass

def shutdown ():
	global mainsock, recEngine
	if threading.activeCount () > 1:
		thread.exit ()
	if mainsock is not None:
		mainsock.close ()
		mainsock = None
	try:
		os.unlink (recEngine.socketpath)
	except OSError:
		pass
	sys.exit ()

def __shutdownAndExit (signum, frame):
	stop ()

def start ():
	global mainsock, recEngine, _pollThread, pckt
	recEngine = recorder ()
	mainsock = recEngine.start ()

	# Start polling server
	_pollThread = PollServer ()
	_pollThread.setDaemon (True)
	_pollThread.start ()
	atexit.register (_pollThread.shutdown)
	atexit.register (shutdown)
	try:
		ldtplib.ldtprecorder.start (pckt)
	except:
		print traceback.format_exc ()
		stop ()

def stop ():
	global mainsock, recEngine
	if mainsock is not None:
		mainsock.close ()
		mainsock = None
	try:
		os.unlink (recEngine.socketpath)
	except OSError:
		pass
	ldtplib.ldtprecorder.stop ()
	recEngine.stop ()

# Contains poll fd's
_serverpoll = None
_pollThread = None

clientFd  = None
mainsock  = None
recEngine = None

# Send lock
sendLck = threading.Lock ()
# Recieve lock
recvLck = threading.Lock ()

ldtpDebug = os.getenv ('LDTP_DEBUG') # Enable debugging

signal.signal (signal.SIGINT, __shutdownAndExit)
signal.signal (signal.SIGQUIT, __shutdownAndExit)
pckt = packet ()
try:
	# Start recording service
	start ()
except:
	print traceback.format_exc ()
	stop ()
