#!/usr/bin/python
# @package      hubzero-python
# @file         hzcms-tick.py
# @author       David Benham <dbenham@purdue.edu>
# @author       Nicholas J. Kisseberth <nkissebe@purdue.edu>
# @copyright    Copyright (c) 2013 HUBzero Foundation, LLC.
# @license      http://www.gnu.org/licenses/lgpl-3.0.html LGPLv3
#
# Copyright (c) 2013 HUBzero Foundation, LLC.
#
# This file is part of: The HUBzero(R) Platform for Scientific Collaboration
#
# The HUBzero(R) Platform for Scientific Collaboration (HUBzero) 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 3 of the License, or (at your option) any
# later version.
#
# HUBzero 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, see <http://www.gnu.org/licenses/>.
#
# HUBzero is a registered trademark of HUBzero Foundation, LLC.
#

import argparse
import datetime
import errno
import fcntl
import hubzero.config.hubzerositeconfig
import hubzero.config.webconfig
import hubzero.utilities.misc
import os
import sys
import time
import pycurl
import StringIO
import string
from dateutil.tz import tzlocal
import socket
import json

class LockException(Exception):
    """
    Custom exception thrown by HubLock class when a lock file is 
    unavailable
    """
    pass


class HubLock:
    """
    Class used for singleton resource access. Two ways to use this class:
    
    1) create simple object:
    lock = HubLock("hub.tick.tock.lock")
    ...
    lock = Nothing
    
    Lock is created in consturctor, released when the destructor is called
    
    2) Use with for context based control:
    with HubLock("hub.tick.tock.lock") as l:
    ...
    
    Enter and exit methods manage the lock access. Use this case when lock
    contention is high and you need quick release
    
    """
    
    lockName = ""
    fd = None
    pid = 0
    locked = False
    lockDir = "/var/lock/"

    def lock(self):
        try:
            if not self.locked:
                if not self.staleLock():
                    self.fd = open(self.lockDir + self.lockName, 'w', 0)
                    fcntl.flock(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
                    self.fd.write(str(self.pid))
                    self.fd.flush
                    self.locked = True
        except IOError as e:
            if e.errno == errno.EAGAIN:
                raise LockException("File " + lockDir + self.lockName + " is locked")
            else:
                raise
        
    def unlock(self):
        if self.locked:
            # closing unlocks the file
            self.fd.close()
        
            os.remove(self.lockDir + self.lockName)
            
            self.locked = False

    def staleLock(self):
        """
        If a lock file is present, this checks the timestamp of the lock's creation 
        against the system up time. If timestamp is older than system uptime, the
        lockfile is considered stale and deleted. Else, the lock is considered
        valid and an exception is raised.
        
        Note, this function can detect a lock by a file's existence. Normally
        we do an flock on the file as well and delete the file altogether when
        the immediately after the lock is released, but after a catastrophic failure
        a flock would be released after a reboot yet the file would still be in the
        filesystem
        
        Although, since we put our locks in /var/lock, this is a temp filesystem
        that should get cleared out after an actual reboot, so this code is likely
        never to get hit, unless you put your lock in another directory
        """

        lockfile = self.lockDir + self.lockName

        if not os.path.exists(lockfile):
            return False
        
        fd = open(lockfile, 'r')
        line = fd.readline()
        try:
            storedpid = int(line)
        except:
            storedpid = -1
        fd.close
    
        if storedpid > 0:
            with open('/proc/uptime', 'r') as f:
                uptimeSeconds = float(f.readline().split()[0])
    
            # compare lock file age to the pid age, if the file is older, assume a
            # reboot orphaned the lock file and delete it

            lockfileAgeSeconds = (time.mktime(time.localtime()) - 
                os.path.getmtime(lockfile))
            
            if (lockfileAgeSeconds > uptimeSeconds):
                os.remove(lockfile)
                return True
            else:
                raise LockException("File " + lockfile + " is locked (" + 
                    str(lockfileAgeSeconds) + "<" + str(uptimeSeconds) +")")
        else:
            return False
        
        
    def __init__(self, lockName):
        self.lockName = lockName
        self.pid = os.getpid()
        self.lock()
        
    def __del__(self):
        self.unlock()
            
    def __enter__(self):
        # probably unnecessary due to constructor, but including it seemed like
        # good form
        self.lock()
        return self
    
    def __exit__(self, type, value, traceback):
        self.unlock()

####################################################### 

hostname = socket.gethostbyaddr(socket.gethostname())[0]
hubname = hubzero.config.webconfig.getDefaultSite()
uri = hubzero.config.hubzerositeconfig.getHubzeroConfigOption(hubname, "uri")
page = "/cron/tick"

parser = argparse.ArgumentParser()
parser.add_argument("--hubname", help="hubname to run cron jobs against",
    default=None)
parser.add_argument("--uri", help="uri to run cron jobs against", default=None)
parser.add_argument("--page", help="path of webfile to request", default=None)
args = parser.parse_args()

if args.uri != None:
    uri = args.uri

if args.hubname != None:
    hubname = args.hubname

if args.page != None:
    page = args.page

if not uri:
    errstr = datetime.datetime.now(tzlocal()).isoformat() + " " + hostname + " hzcms-tick[%d]" % os.getpid() + " " + hubname +     " Unable to determine URI to contact.\n"
    sys.stdout.write(errstr)
    #sys.stderr.write(errstr)
    sys.exit(1)

if not page:
    errstr = datetime.datetime.now(tzlocal()).isoformat() + " " + hostname + " hzcms-tick[%d]" % os.getpid() + " " + hubname + " Unable to determine page to request.\n"
    sys.stdout.write(errstr)
    #sys.stderr.write(errstr)
    sys.exit(2)

if not hubname:
    errstr = datetime.datetime.now(tzlocal()).isoformat() + " " + hostname + " hzcms-tick[%d]" % os.getpid() + " " + hubname + " Unable to determine hubname.\n"
    sys.stdout.write(errstr)
    #sys.stderr.write(errstr)
    sys.exit(3)

uri = string.rstrip(uri, '/') + '/'
page = string.lstrip(page, '/')
    
try:

    with HubLock("hub.lock") as l:    

        storage = StringIO.StringIO()
        hdr = StringIO.StringIO()
        c = pycurl.Curl()
        c.setopt(c.URL, uri + page)
        c.setopt(c.SSL_VERIFYHOST, 0)
        c.setopt(c.SSL_VERIFYPEER, 0)
        c.setopt(c.WRITEFUNCTION, storage.write)
        c.setopt(c.HEADERFUNCTION, hdr.write)
        start = datetime.datetime.now(tzlocal())
        c.perform()

        status_line = hdr.getvalue().splitlines()[0]
        
        if (c.getinfo(c.HTTP_CODE) == 200):

            jobs = json.loads( storage.getvalue().splitlines()[0] )

            count = len(jobs['jobs'])

            sys.stdout.write(start.isoformat() + " " + 
                hostname + " hzcms-tick[%d]" % os.getpid() + " " + hubname + " " +
                "GET " + uri + page + " %f %d " % (c.getinfo(c.TOTAL_TIME), count) + 
                status_line + "\n")

            for i in range(count):
                line = "%s, %s, %s, %s, %s, %s, %s, %s" % (
                    i, 
                    jobs['jobs'][i]['id'], 
                    jobs['jobs'][i]['title'], 
                    jobs['jobs'][i]['plugin'], 
                    jobs['jobs'][i]['event'], 
                    jobs['jobs'][i]['last_run'], 
                    jobs['jobs'][i]['next_run'], 
                    jobs['jobs'][i]['active'])

                sys.stdout.write(datetime.datetime.now(tzlocal()).isoformat() + " " + 
                    hostname + " hzcms-tick[%d]" % os.getpid() + " " + hubname + " " + 
                    line + "\n") 
        else:

            errstr = start.isoformat() + " " + hostname + " hzcms-tick[%d]" % os.getpid() + " " + hubname + " " + "GET " + uri + page + " %f 0" % c.getinfo(c.TOTAL_TIME) + status_line + "\n"
            sys.stdout.write(errstr)
            #sys.stderr.write(errstr)
            sys.exit(4)

except LockException as e:
    errstr = datetime.datetime.now(tzlocal()).isoformat() + " " + hostname + " hzcms-tick[%d]" % os.getpid() + " " + hubname + " " + "GET " + uri + page + " 0.000000 0 EXCEPTION LockException %s.\n" % (e)
    sys.stdout.write(errstr)
    #sys.stderr.write(errstr)

    sys.exit(5)

except:
    errstr = datetime.datetime.now(tzlocal()).isoformat() + " " + hostname + " hzcms-tick[%d]" % os.getpid() + " " + hubname + " " + "GET " + uri + page + " 0.000000 0 EXCEPTION UnexpectedException %s.\n" % (sys.exc_info()[0])
    sys.stdout.write(errstr)
    #sys.stderr.write(errstr)

    sys.exit(6)

sys.exit(0)
