#!/usr/bin/python3.9
'''
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.warning(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.warning(in_file + " not HDF5, removed.")
                removed = remove_file(in_file)
        else:
            pyinotify.log.warning(in_file + " is zero length, removed.")
            removed = remove_file(in_file)
    else:
        pyinotify.log.warning(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)
