#!/usr/bin/python
# @package      hubzero-python
# @file         hzcms-tick
# @author       David Benham <dbenham@purdue.edu>
# @author       Nicholas J. Kisseberth <nkissebe@purdue.edu>
# @copyright    Copyright (c) 2013-2015 HUBzero Foundation, LLC.
# @license      http://opensource.org/licenses/MIT MIT
#
# Copyright (c) 2013-2015 HUBzero Foundation, LLC.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# HUBzero is a registered trademark of HUBzero Foundation, LLC.
#

import sys
import os

dist_packages = "/usr/lib/python%d.%d/dist-packages" % (sys.version_info[0], sys.version_info[1])

if os.path.isdir(dist_packages):
    sys.path.insert(1,dist_packages)

import argparse
import datetime
import errno
import fcntl
import hubzero.config.hubzerositeconfig
import hubzero.config.webconfig
import hubzero.utilities.misc
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()

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

os.umask(077)

try:
    hostname = socket.gethostbyaddr(socket.gethostname())[0]
except:
    try:
        hostname = os.uname()[1]
    except:
        hostname = 'localhost'

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.exit(0)

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.exit(0)

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.exit(0)

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.FOLLOWLOCATION, 1)
        c.setopt(c.WRITEFUNCTION, storage.write)
        c.setopt(c.HEADERFUNCTION, hdr.write)
        start = datetime.datetime.now(tzlocal())
        c.perform()
   
        for line in hdr.getvalue().splitlines():
            if line.startswith('HTTP'):
                status_line = line
        
        if (c.getinfo(c.HTTP_CODE) == 200):

            try:
                jsonstart = string.index(storage.getvalue(),'{')
                jobs = json.loads( storage.getvalue()[jsonstart:] )
                count = len(jobs['jobs'])
                strcount = str(count)
            except:
                count = 0
                strcount = '-'

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

            for i in range(count):
                line = "%s, %s, %s, %s, %s, %s, %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'],
                    jobs['jobs'][i]['start_time'],
                    jobs['jobs'][i]['start_mem'],
                    jobs['jobs'][i]['end_time'],
                    jobs['jobs'][i]['end_mem'],
                    jobs['jobs'][i]['delta_time'],
                    jobs['jobs'][i]['delta_mem'])

                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 - " % c.getinfo(c.TOTAL_TIME) + status_line + "\n"
            sys.stdout.write(errstr)

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

sys.exit(0)
