#!/usr/bin/python3.6
'''
Copyright (C) 2010- Swedish Meteorological and Hydrological Institute (SMHI)

This file is part of RAVE.

RAVE 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.

RAVE 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 RAVE.  If not, see <http://www.gnu.org/licenses/>.
------------------------------------------------------------------------*/

Daemon for monitoring an input direcory for ODIM_H5 files and injecting them
into a BALTRAD node securely.

The original use of this was also for converting files from NORDRAD2, thefore
the name ...

This functionality can be considered a boilerplate for doing whatever you want
with inotify and ODIM_H5 files. Just add your own functionality in the MAIN
function.

This version injects files to a secure BaltradDex.
Make sure you have followed the instructions on how to transmit a security
certificate to the DEX prior to using this.

This daemon uses pyinotify.

@file
@author Daniel Michelson, SMHI
@date 2012-01-08
'''
import sys, os
import pyinotify
import _raveio
import _rave
import math
from rave_defines import DEX_SPOE  # Files are injected to this URI

BLTROOT = '/var/lib/baltrad'  # CHANGE if necessary
ODIM_INJECTOR_ROOT = BLTROOT + '/odim_injector'  # CHANGE if necessary
DEFAULTIN = ODIM_INJECTOR_ROOT + '/data'  # CHANGE if necessary
PIDFILE = ODIM_INJECTOR_ROOT + '/odim_injector.pid'
LOGFILE = ODIM_INJECTOR_ROOT + '/odim_injector.log'
LOGFILESIZE = 5000000  # 5 Mb each
LOGFILES = 5

MASK = pyinotify.IN_CLOSE_WRITE


# # Determines whether the daemon is running, based on the PID in the PIDFILE.
# @return True if the daemon is running, otherwise False
def alive(pidfile):
  if os.path.isfile(pidfile):
    fd = open(pidfile)
    c = fd.read()
    fd.close()
    try:
      pgid = os.getpgid(int(c))
      return True
    except:
      return False
  else:
    return False


# # Kills the daemon, first softly, then hard if necessary.
def killme(pidfile):
  import signal
  try:
    fd = open(options.pid_file)
    c = fd.read()
    fd.close()
    try:
      os.kill(int(c), signal.SIGHUP)
    except:
      os.kill(int(c), signal.SIGKILL)
    os.remove(options.pid_file)
  except:
    print("Could not kill daemon. Check pid.")

# # Processes all the files that have arrived in the input directory.
# While catchup() is grinding through a long list of files, new ones can
# arrive and they'll be ignored unless this functionality is looped.
# @param in_dir string containing the input directory to be monitored
# @param janitor boolean saying whether or not to delete inbound files
# @param uri string containing the URI of the BALTRAD node to which to inject
def catchup(in_dir, janitor, uri):
  import glob
  not_removed = []
  while 1:
    flist = sorted(glob.glob(os.path.join(in_dir, '*')), key=os.path.getmtime, reverse=True)
    if len(flist) == 0 or set(flist) == set(not_removed):
      break
    else:
      not_removed = []
      for fstr in flist:
        if not MAIN(fstr, janitor, uri):
          not_removed.append(fstr)

# # Checks if the file is an ODIM_H5 file. The only real verification is
# the /Conventions attribute, which isn't good but will suffice for now...
# @param filename string containing the name of the input file to query
# @return the string in the /Conventions attribute or None if none there
def isODIM(filename):
  import _pyhl
  try:
    a = _pyhl.read_nodelist(filename)
    a.selectNode('/Conventions')
    a.fetch()
    b = a.getNode('/Conventions')
    return b.data()
  except:
    return None

def remove_file(fname):
  try:
    os.remove(fname)
    return True
  except Exception as e:
    pyinotify.log.error("Failed to remove file '%s': %s" % (fname, e.__str__()))
  return False

# # Main function, queries the input file, injects it to the BALTRAD node if
# it is ODIM_H5 and the "janitor" is turned off. The input file is deleted
# afterwards.
# @param in_file string containing the full path and file name of an input file
# @param janitor boolean saying whether or not to delete this file instead of
# inject it
# @param uri string containing the URI of the BALTRAD node to which to inject
# @return True if file successfully was removed, otherwise Falsae
def MAIN(in_file, janitor=False, uri=DEX_SPOE):
  """
  The main action to take within the main loop.
  Assume that in_file contains an absolute path.
  """
  import _pyhl
    
  removed = False
    
  if os.path.isfile(in_file):
    if os.path.getsize(in_file) != 0:
      if _pyhl.is_file_hdf5(in_file):
        if janitor:
          pyinotify.log.info("Janitor: %s" % in_file)
          removed = remove_file(in_file)
        else:
          try:
            import BaltradFrame, odim_source
            if isODIM(in_file):
              rio = _raveio.open(in_file)
              this = rio.object
              s = odim_source.ODIM_Source(this.source)
              if rio.objectType == _rave.Rave_ObjectType_SCAN:
                pyinotify.log.info("SCAN: %s %sT%sZ angle=%2.1f" % (s.nod,
                                                                    this.date,
                                                                    this.time,
                                                                    this.elangle * 180.0 / math.pi))
              elif rio.objectType == _rave.Rave_ObjectType_PVOL:
                pyinotify.log.info("PVOL: %s %sT%sZ" % (s.nod,
                                                        this.date,
                                                        this.time))
              else:
                pyinotify.log.info("Unknown ODIM file")
                            

              # Send file to BALTRAD
              try:
                BaltradFrame.inject_file(in_file, DEX_SPOE)
              except Exception as e:
                pyinotify.log.error("Failed to inject %s. Error message: %s" % (in_file, e))
              if os.path.isfile(in_file):
                removed = remove_file(in_file)
            else:
              pyinotify.log.warn(in_file + " not ODIM_H5, removed.")
              removed = remove_file(in_file)
          except Exception as e:
            pyinotify.log.error("%s" % e)
            removed = remove_file(in_file)

      else:
        pyinotify.log.warn(in_file + " not HDF5, removed.")
        removed = remove_file(in_file)
    else:
      pyinotify.log.warn(in_file + " is zero length, removed.")
      removed = remove_file(in_file)
  else:
    pyinotify.log.warn(in_file + " not a regular file, ignored.")
    
  return removed

# This class, and especially its method, overrides the default process
# in (py)inotify
class OdimInjector(pyinotify.ProcessEvent):
  # # Initializer
  # @param options variable options list
  def __init__(self, options):
    self.options = options

  # # Inherited from pyinotify
  # @param event object containing a path, probably ...
  def process_IN_CLOSE_WRITE(self, event):
    pyinotify.log.info("IN_CLOSE_WRITEr: %s" % event.pathname)
    MAIN(event.pathname, janitor=self.options.janitor, uri=self.options.dex_uri)


if __name__ == "__main__":
  from optparse import OptionParser
  import logging, logging.handlers

  usage = "usage: odim_injector -i <input dir> -p <pidfile> -l <logfile> [hkcj]"
  usage += ""
  parser = OptionParser(usage=usage)

  parser.add_option("-i", "--indir", dest="in_dir",
                    default=DEFAULTIN,
                    help="Name of input directory to monitor.")

  parser.add_option("-u", "--dex_uri", dest="dex_uri",
                    default=DEX_SPOE,
                    help="The URI of the BALTRAD node in which to inject files. Defaults to the URI given in rave_defines.DEX_SPOE.")

  parser.add_option("-p", "--pidfile", dest="pid_file",
                    default=PIDFILE,
                    help="Name of PID file to write.")

  parser.add_option("-l", "--logfile", dest="log_file",
                    default=LOGFILE,
                    help="Name of rotating log file.")

  parser.add_option("-c", "--catchup", action="store_true", dest="catchup",
                    help="Process all files that have collected in the input directory. Otherwise only act on new files arriving.")

  parser.add_option("-j", "--janitor", action="store_true", dest="janitor",
                    help="Remove files that arrive in the input directory.")

  parser.add_option("-k", "--kill", action="store_true", dest="kill",
                    help="Attempt to kill a running daemon.")

  (options, args) = parser.parse_args()
  if not os.path.isdir(options.in_dir):
    print("odim_injector in_dir="+options.in_dir+" does not exist")
    sys.exit()

  if not options.kill:
    ALIVE = alive(options.pid_file)
    if not ALIVE and os.path.isfile(options.pid_file):
      print("odim_injector is not alive but pid file %s exists, removing." % options.pid_file)
      os.remove(options.pid_file)
    elif ALIVE:
      print("odim_injector is already running.")
      sys.exit()

    # Shut down a previous incarnation of this daemon.
  if options.kill:
    killme(options.pid_file)
    sys.exit()

    # Start the logging system
  pyinotify.log.setLevel(logging.INFO)
  handler = logging.handlers.RotatingFileHandler(options.log_file,
                                                 maxBytes=LOGFILESIZE,
                                                 backupCount=LOGFILES)
  formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s',
                                '%Y-%m-%d %H:%M:%S %Z')
  handler.setFormatter(formatter)
  pyinotify.log.addHandler(handler)
    
  # Process files that have arrived since I was running last.
  if options.catchup:
    catchup(options.in_dir, options.janitor, options.dex_uri)

  wm = pyinotify.WatchManager()
  notifier = pyinotify.Notifier(wm, OdimInjector(options))

  # Only act on closed files, or whatever's been moved into in_dir
  wm.add_watch(options.in_dir, MASK)

  notifier.loop(daemonize=True, pid_file=options.pid_file)
