#!/usr/bin/env python
import sys
import os
import datetime
import time
import select
import logging
import libxml2
import getpass
import ConfigParser
from subprocess import Popen, PIPE
from optparse import *
from M2Crypto.SSL import Context

# pubsub import must come first because it overloads part of the
# StanzaProcessor class
from ligo.lvalert import pubsub

from pyxmpp.all import JID,Iq,Presence,Message,StreamError,TLSSettings
from pyxmpp.jabber.all import Client
from pyxmpp.jabber.simple import send_message
from pyxmpp.interface import implements
from pyxmpp.interfaces import *

"""
A tool to listen for events on a pubsub node 
"""

__version__ = "$Revision$"
__date__ = "$Date$"
__name__ = "lvalert_listen"
__Id__ = "$Id$"
__title__ = "LIGO-Virgo Alert Administration"


#################################################################
# help message
usage = """\
%prog [options]
-----------------------------------------------------------------

  A tool to listen to the pubsub service of openfire at
  lvalert.cgca.uwm.edu. LSC-Virgo members can activate their accounts 
  on this server by completing the form at
  
    https://www.lsc-group.phys.uwm.edu/cgi-bin/jabber-acct.cgi 

  and typing your password.

  Before using this program to listen to a node, you must subscribe to
  the node using lvalert_admin. Then you will receive any events that are
  published to that node by doing:

  %prog --username albert.einstein --password secret

  When an event is published to the node, a message will be printed to
  the window where the listener is running. To see the event, run

  %prog --username albert.einstein --password secret --show

  The owner (person who creates the node by default) can delete and
  publish information to the node. The owner can also add other
  publishers to the node. Configuration and management of nodes and
  subscriptions are handled with lvalert_admin. 
  
  Others can subscribe to any existing node. Run 

  lvalert_admin --help

  to find out how to manage your subscriptions. 

  It is also possible to specify actions to be taken upon receipt of a
  message from a given node using a config-file to specify the program
  to run when the message is received via different nodes. The payload
  of the message is piped to the command via stdin.  A sample
  config-file called example.ini might look like

  [lvalert_test]
  executible=./mycounter

  then running

  lvalert_listen --username albert.einstein --password secret --config-file example.ini

  would result in ./mycounter being executed and the output printed to
  screen whenever a message was received via the lvalert_test node. 

  Alternatively, instead of the name of an executable, you could
  indicate either "stdout" or "-" (withouth quotes) and the alert
  payload will be written to standard output.
"""

#################################################################
def parse_command_line():
  """
  Parser function dedicated
  """
  parser = OptionParser( usage=usage, \
      version= "%prog CVS\n" +
      "$Id$\n" +
      "$Name$\n")

  #username and password
  parser.add_option("-a","--username",action="store",type="string",\
      default="", help="the username of the publisher or listener" )
  parser.add_option("-b","--password",action="store",type="string",\
      default="", help="the password of the publisher or listener" )
  parser.add_option("-s","--server",action="store",type="string",\
      default="lvalert.cgca.uwm.edu", help="the pubsub server" )
  parser.add_option("-r","--resource",action="store",type="string",\
      default="listener", help="resource to use in JID" )
  parser.add_option("-c","--config-file",action="store",type="string",\
      default=None, help="config file with list of actions" )

  parser.add_option("-S","--show",action="store_true",\
      default=False, help="print the payload to stdout" )

  parser.add_option("-n","--node",action="store",type="string",\
      default=None, help="name of the node on the pubsub server" )

  # debugging options
  parser.add_option("-v","--verbose",action="store_true",\
      default=False, help="be verbose as you process the request" )
  parser.add_option("-g","--debug",action="store_true",\
      default=False, help="should  print out lots of information" )

  # options about message handling
  parser.add_option("", "--dont-wait",action="store_true",\
      default=False, help="if supplied, jobs will be launched \
      as soon as they arrive, rather than blocking")
  
  (options,args) = parser.parse_args()

  return options, sys.argv[1:]
  
# ============================================================================
# -- get command line arguments
opts, args = parse_command_line()

if opts.dont_wait: ### required packages for "dont-wait" option
    import multiprocessing
    import tempfile

#=============================================================================
### R. ESSICK 12/2/2014
def forked_wait(cmd, file_obj):
    """
    used with the "--dont-wait" option to avoid zombie processes via a double fork
    main process will wait for this function to finish (quick), send the "wait" signal
    which removes this function from the process table, and then moves on.
    the forked process that this function creates become orphaned, and will automatically
    be removed from the process table upon completion.
    """
    Popen(cmd, stdin=file_obj)

#=======================

class LVAlertHandler(object):
    """Provides the actions taken when an event arrives.
    """

    implements(IMessageHandlersProvider)
    
    def __init__(self, client, actions, wait=True):
        """Just remember who created this."""
        self.client = client
        self.actions = actions
        self.wait = wait
    
    def get_message_handlers(self):
        """Return list of (message_type, message_handler) tuples.

        The handlers returned will be called when matching message is received
        in a client session."""
        return [
            (None, self.message),
            ]

    def message(self,stanza):
        """Message handler for the component.

        Prints a message with the time an alert is received. If the
        --show option is set, then it will also print the contents of
        the alert.

        if self.wait: blocks until this processes terminates and prints the returned string
        else: does not block, and multiple events will be launched and processed as soon as they are read

        :returns: `True` to indicate, that the stanza should not be processed
        any further."""
        e=self.get_entry(stanza)
        n=self.get_node(stanza)
        if e:
            if n in self.actions:
                action = self.actions[n]
                if action == 'stdout' or action == '-':
                    sys.stdout.write(str(e))
                #===================================================================
                ### changes made by R. Essick (ressick@mit.edu) and R. Vaulin (vaulin@ligo.org)
                ###
                ### this avoids using p.communicate(), which blocks and prevents the 
                ### alert handler from launching another processes until this one finishes
                ### we use "spooled" tempfiles because they exist only in memory (if small enought)
                ### max_size=1000 (bytes?) should be larger than most expected lvalert messages?
                elif self.wait:
                    p = Popen([self.actions[n]], stdin=PIPE, stdout=PIPE)
                    print p.communicate(e)[0]
                else:
#                    import tempfile
                    file_obj = tempfile.SpooledTemporaryFile(mode="w+r", max_size=1000)
                    file_obj.write(e)
                    file_obj.readlines() ### bug fix for "alert_type"=="new" events
                                         ### without this, the position in file_obj gets messed up
                                         ### no idea why, but it might be related to long messages becoming multiple stanzas
                    file_obj.seek(0, 0)
#                    p = Popen([self.actions[n]], stdin=file_obj)
                    p = multiprocessing.Process(target=forked_wait, args=([self.actions[n]], file_obj))
                    p.start()
                    p.join()
                    file_obj.close()
                #===================================================================
                sys.stdout.flush()
            else:
                print "Payload received at %s" % (datetime.datetime.now().ctime())
                if opts.show:
                    print u'%s' % (e,),
        return True

    def get_node(self,stanza):
        c = stanza.xmlnode.children
        c = c.children
        if c:
            return c.prop("node")

    def get_entry(self,stanza):
        c = stanza.xmlnode.children
        while c:
            try:
                if c.name=="event":
                    return c.getContent()
            except libxml2.treeError:
                pass
            c = c.next
        return None

class MyClient(Client):
    def __init__(self, jid, password,actions, wait=True):
        # if bare JID is provided add a resource -- it is required
        if not jid.resource:
            jid=JID(jid.node, jid.domain, "listener")

        # we require a TLS connection
        #  Specify sslv3 to get around Sun Java SSL bug handling session ticket
        #  https://rt.phys.uwm.edu/Ticket/Display.html?id=1825
        #  http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6728126
        # NOTE: This is no longer necessary after Openfire 3.7.
        # (See update to RT ticket above.)
        #t=TLSSettings(require=True,verify_peer=False, ctx=Context('sslv3'))
        t=TLSSettings(require=True,verify_peer=False)

        # setup client with provided connection information
        # and identity data
        Client.__init__(self, jid, password, \
            auth_methods=["sasl:GSSAPI","sasl:PLAIN"], tls_settings=t,keepalive=30)

        # add the separate components
        self.interface_providers = [
            LVAlertHandler(self,actions, wait=wait),
            ]

    def stream_state_changed(self,state,arg):
        """This one is called when the state of stream connecting the
        component to a server changes. This will usually be used to
        let the user know what is going on."""
        if opts.verbose:
            print "*** State changed: %s %r ***" % (state,arg)
        else:
            pass

# add a logger so that we can see what's going
if opts.debug:
    logger=logging.getLogger()
    logger.addHandler(logging.StreamHandler())
    logger.setLevel(logging.DEBUG)
else:
    logging.basicConfig(level=logging.ERROR)

# debug the memory
libxml2.debugMemory(1)

# set up handlers for each node
actions={}
if opts.config_file:
    cp=ConfigParser.ConfigParser()
    cp.read(opts.config_file)
    for node in cp.sections():
        try:
            actions[node] = cp.get(node,'executible')
        except ConfigParser.NoOptionError:
            actions[node] = cp.get(node,'executable')

# set up the stream
myjid=JID(opts.username+"@"+opts.server+"/"+opts.resource)
if not opts.password:
    mypassword = getpass.getpass()
else:
    mypassword = opts.password
s=MyClient(myjid,mypassword,actions, wait=not opts.dont_wait)

if opts.verbose:
    print "connecting..."
s.connect()

if opts.verbose:
    print "listening for message..."
try:
    s.loop(1)
except KeyboardInterrupt:
    print u"disconnecting..."
    s.disconnect()

# vi: sts=4 et sw=4
