#!/usr/bin/env python

#############################################################################
#
#  Linux Desktop Testing Project http://ldtp.freedesktop.org
# 
#  Author:
#     Nagappan Alagappan <nagappan@gmail.com>
# 
#  Copyright 2004 - 2007 Novell, Inc.
#  Copyright 2008 - 2009 Nagappan Alagappan
# 
#  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 Alagappan <nagappan@gmail.com>"
# __maintainer__ = "Nagappan Alagappan <nagappan@gmail.com>"
__version__ = "1.5.0"

import os
import re
import sys
import xml
import time
import types
import struct
import socket
import thread
import select
import signal
import traceback
import threading

import ldtplib.ldtprecorder
import ldtplib.ldtplibutils

from xml.sax import saxutils
from xml.parsers.expat import ExpatError
from xml.dom.minidom import parse, parseString

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

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):
        self._fdList = {}
        self._clientFd = None
        self._notificationClientFd = None

    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
        if windowName is not None:
            _xml = _xml + '<WINDOWNAME>' + \
                saxutils.escape (ldtplib.ldtplibutils.escapeChars (windowName,  False)) + \
                '</WINDOWNAME>'
        if objectName is not None:
            if re.search (';',  objectName) != None and re.search ('\:',  objectName) != None:
                _xml = _xml + '<OBJECTNAME>' + \
                    saxutils.escape (objectName) + '</OBJECTNAME>'
            else:
                _xml = _xml + '<OBJECTNAME>' + \
                    saxutils.escape (ldtplib.ldtplibutils.escapeChars (objectName)) + \
                    '</OBJECTNAME>'
        if objectType is not None:
            _xml = _xml + '<OBJECTTYPE>' + objectType + '</OBJECTTYPE>'
        if eventType is not None:
            _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"""
        _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, notification = True):
        if self._clientFd is None and self._notificationClientFd is None:
            return
        flag = False
        try:
            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
                if _ldtpDebug:
                    print 'packet:', msg
                buf = ''
                try:
                    buf = msg.encode ('utf-8')
                except UnicodeDecodeError:
                    buf = unicode (msg, 'utf-8').encode ('utf-8')
    
                _length =  len (buf)
                # Pack length (integer value) in network byte order
                format = '!i%ds' % _length
                _msg = struct.pack (format, _length, buf)
                # Send message
                if notification:
                    self._notificationClientFd.send (_msg)
                else:
                    self._clientFd.send (_msg)
                if _ldtpDebug != None and _ldtpDebug == '2':
                    print 'Send packet', buf
                #_sendLck.release ()
            except socket.error, msg:
                #raise LdtpExecutionError ('Client aborted')
                return
        finally:
            if flag == True:
                # Reason for using the flag:
                # 'Do not call this method when the lock is unlocked.'
                _sendLck.release ()
                flag = False

    def recvpacket (self, sockfd = None):
        flag = False
        try:
            try:
                _recvLck.acquire ()
                flag = True
                _client = None
                # Get client socket fd based on thread id
                if sockfd == None:
                    _client = self._clientFd
                else:
                    if sockfd in self._fdList:
                        _client = self._fdList [sockfd]
                    else:
                        if _ldtpDebug:
                            print 'Invalid socket fd %d' % sockfd
                        return ''
                _responsePacket = None
                _client.settimeout (5.0)
                # Hardcoded 4 bytes, as the server sends 4 bytes as packet length
                data = _client.recv (4)
                if data == '' or data == None:
                    return None
                _packetSize, = struct.unpack('!i', data)
                if _ldtpDebug != None and _ldtpDebug == '2':
                    print 'Received packet size', _packetSize
    
                _responsePacket = _client.recv (_packetSize)
                #_recvLck.release ()
                if _ldtpDebug != None and _ldtpDebug == '2':
                    print 'Received response Packet', _responsePacket
                return _responsePacket
            except struct.error, msg:
                raise LdtpExecutionError ('Invalid packet length ' + str (msg))
            except socket.timeout:
                if _ldtpDebug != None and _ldtpDebug == '2':
                    print 'Timeout'
                return ''
            except:
                if hasattr (traceback, 'format_exc'):
                    raise LdtpExecutionError ('Error while receiving packet ' + str (traceback.format_exc ()))
                else:
                    raise LdtpExecutionError ('Error while receiving packet ' + str (traceback.print_exc ()))
        finally:
            if flag == True:
                # Reason for using the flag:
                # 'Do not call this method when the lock is unlocked.'
                _recvLck.release ()
                flag = False

class PollServer (threading.Thread):
    def __init__ (self, mainSock, pckt):
        threading.Thread.__init__ (self)
        self._events = None
        self._serverpoll = None
        self._mainsock = mainSock
        self._pckt = pckt

    def run (self):
        try:
            self.start_polling_server ()
        except:
            if hasattr (traceback, 'format_exc'):
                raise LdtpExecutionError (str (traceback.format_exc ()))
            else:
                raise LdtpExecutionError (str (traceback.print_exc ()))

    def start_polling_server (self):
        self._serverDisconnected = False
        self._serverpoll = select.poll ()
        self._serverpoll.register (self._mainsock, select.POLLIN)

        while True:
            try:
                self._events = self._serverpoll.poll ()
            except socket.error, msg:
                break
            except:
                self._serverpoll.unregister (self._mainsock)
                self._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):
                        if self._events [i][0] == self._mainsock.fileno ():
                            _clientFd, addr = self._mainsock.accept ()
                            self._pckt._fdList [_clientFd.fileno ()] = _clientFd
                            self._serverpoll.register (_clientFd, select.POLLIN)
                            continue
                        try:
                            if self.handlePacket (self._events [i][0]) == None:
                                self._serverpoll.unregister (self._events[i][0])
                                continue
                        except LdtpExecutionError, msg:
                            self._serverDisconnected = True
                            break
                        except SystemExit:
                            self._serverDisconnected = True
                            break
                        except:
                            if _ldtpDebug:
                                if hasattr (traceback, 'format_exc'):
                                    print traceback.format_exc ()
                                else:
                                    print traceback.print_exc ()
                    elif (self._events[i][1] & select.POLLNVAL):
                        # Unregister newly created socket from polling once its completed
                        self._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:
                if _ldtpDebug:
                    if hasattr (traceback, 'format_exc'):
                        print traceback.format_exc ()
                    else:
                        print traceback.print_exc ()
                self._serverpoll.unregister (self._mainsock)
                sys.exit ()
            except socket.error:
                if _ldtpDebug:
                    if hasattr (traceback, 'format_exc'):
                        print traceback.format_exc ()
                    else:
                        print traceback.print_exc ()
    def handlePacket (self, sockFd):
        try:
            self._packet = self._pckt.recvpacket (sockFd)
            if _ldtpDebug:
                print 'handlePacket',  self._packet
        except KeyboardInterrupt:
            return None
        except LdtpExecutionError:
            return ''
        try:
            if self._packet == None:
                return None
            if self._packet != '':
                _requestId, _commandCode, _name = self._pckt.parsexml (self._packet)
                if _commandCode == ldtplib.ldtplibutils.RecordCommand.INVALID:
                    return ''
                if _commandCode == ldtplib.ldtplibutils.RecordCommand.NOTIFICATION:
                    if sockFd in self._pckt._fdList:
                        self._pckt._notificationClientFd = self._pckt._fdList [sockFd]
                    return ''
                if _commandCode == ldtplib.ldtplibutils.RecordCommand.STOP:
                    if sockFd in self._pckt._fdList:
                        self._pckt._clientFd = self._pckt._fdList [sockFd]
                    _status = {}
                    _status [0] = 0
                    _status [1] = 'Success'
                    _xml = self._pckt.generateresponsexml (_requestId, _status)
                    self._pckt.sendpacket (_xml, notification = False)
                    stop ()
                    # thread.exit ()
                if _commandCode == ldtplib.ldtplibutils.RecordCommand.WINDOWEXIST:
                    if sockFd in self._pckt._fdList:
                        self._pckt._clientFd = self._pckt._fdList [sockFd]
                    _status = {}
                    _status [0] = 0
                    _status [1] = 'Success'
                    _xml = ''
                    if ldtplib.ldtprecorder.recorder.doesWindowExist (_name):
                        _xml = self._pckt.generateresponsexml (_requestId, _status)
                    else:
                        _status [0] = 1
                        _status [1] = 'Window does not exist'
                        _xml = self._pckt.generateresponsexml (_requestId, _status)
                    self._pckt.sendpacket (_xml, notification = False)
                    return ''
                if _commandCode == ldtplib.ldtplibutils.RecordCommand.GETWINDOWNAME:
                    if sockFd in self._pckt._fdList:
                        self._pckt._clientFd = self._pckt._fdList [sockFd]
                    if _ldtpDebug:
                        print 'get window name'
                    return ''
                if _commandCode == ldtplib.ldtplibutils.RecordCommand.GETOBJECTNAME:
                    if sockFd in self._pckt._fdList:
                        self._pckt._clientFd = self._pckt._fdList [sockFd]
                    if _ldtpDebug:
                        print 'get object name'
                    _status = {}
                    _status [0] = 0
                    _status [1] = 'Success'
                    _xml = ''
                    _objName = ldtplib.ldtprecorder.recorder.getObjectName (_name)
                    if _ldtpDebug:
                        print '_objName',  _objName
                    if _objName != '':
                        _xml = self._pckt.generateresponsexml (_requestId,
                                                               _status, _objName)
                        if _ldtpDebug: print _xml
                    else:
                        _status [0] = 1
                        _status [1] = 'Object does not exist'
                        _xml = self._pckt.generateresponsexml (_requestId, _status)
                    self._pckt.sendpacket (_xml, notification = False)
                    return ''
        except TypeError:
            return ''
        except LdtpExecutionError:
            return ''
        except KeyboardInterrupt:
            return None
        except:
            if _ldtpDebug:
                if hasattr (traceback, 'format_exc'):
                    print traceback.format_exc ()
                else:
                    print traceback.print_exc ()
        return ''
    def shutdown (self):
        if self._mainsock:
            self._mainsock.close ()
            self._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 (_mainsock, _pckt)
    _pollThread.setDaemon (True)
    _pollThread.start ()
    try:
        ldtplib.ldtprecorder.start (_pckt)
    except:
        if _ldtpDebug:
            if hasattr (traceback, 'format_exc'):
                print traceback.format_exc ()
            else:
                print traceback.print_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 ()

signal.signal (signal.SIGINT, __shutdownAndExit)
signal.signal (signal.SIGQUIT, __shutdownAndExit)
_pckt = packet ()
try:
    # Start recording service
    start ()
except:
    if _ldtpDebug:
        if hasattr (traceback, 'format_exc'):
            print traceback.format_exc ()
        else:
            print traceback.print_exc ()
    stop ()
