# @GPL_HEADER_START
# Use of the RAVE and related source code is subject to the terms
# of the following licenses:
# 
# GNU Public License (GPL) Rights pursuant to Version 2, June 1991
# Government Purpose License Rights (GPLR) pursuant to DFARS 252.225-7013
# 
# NO WARRANTY
# 
# ANY INFORMATION, MATERIALS, SERVICES, INTELLECTUAL PROPERTY OR OTHER
# PROPERTY OR RIGHTS GRANTED OR PROVIDED BY CARNEGIE MELLON UNIVERSITY
# PURSUANT TO THIS LICENSE (HEREINAFTER THE "DELIVERABLES") ARE ON AN
# "AS-IS" BASIS. CARNEGIE MELLON UNIVERSITY MAKES NO WARRANTIES OF ANY
# KIND, EITHER EXPRESS OR IMPLIED AS TO ANY MATTER INCLUDING, BUT NOT
# LIMITED TO, WARRANTY OF FITNESS FOR A PARTICULAR PURPOSE,
# MERCHANTABILITY, INFORMATIONAL CONTENT, NONINFRINGEMENT, OR ERROR-FREE
# OPERATION. CARNEGIE MELLON UNIVERSITY SHALL NOT BE LIABLE FOR INDIRECT,
# SPECIAL OR CONSEQUENTIAL DAMAGES, SUCH AS LOSS OF PROFITS OR INABILITY
# TO USE SAID INTELLECTUAL PROPERTY, UNDER THIS LICENSE, REGARDLESS OF
# WHETHER SUCH PARTY WAS AWARE OF THE POSSIBILITY OF SUCH DAMAGES.
# LICENSEE AGREES THAT IT WILL NOT MAKE ANY WARRANTY ON BEHALF OF
# CARNEGIE MELLON UNIVERSITY, EXPRESS OR IMPLIED, TO ANY PERSON
# CONCERNING THE APPLICATION OF OR THE RESULTS TO BE OBTAINED WITH THE
# DELIVERABLES UNDER THIS LICENSE.
# 
# Licensee hereby agrees to defend, indemnify, and hold harmless Carnegie
# Mellon University, its trustees, officers, employees, and agents from
# all claims or demands made against them (and any related losses,
# expenses, or attorney's fees) arising out of, or relating to Licensee's
# and/or its sub licensees' negligent use or willful misuse of or
# negligent conduct or willful misconduct regarding the Software,
# facilities, or other rights or assistance granted by Carnegie Mellon
# University under this License, including, but not limited to, any
# claims of product liability, personal injury, death, damage to
# property, or violation of any laws or regulations.
# 
# Carnegie Mellon University Software Engineering Institute authored
# documents are sponsored by the U.S. Department of Defense under
# Contract F19628-00-C-0003. Carnegie Mellon University retains
# copyrights in all material produced under this contract. The U.S.
# Government retains a non-exclusive, royalty-free license to publish or
# reproduce these documents, or allow others to do so, for U.S.
# Government purposes only pursuant to the copyright license under the
# contract clause at 252.227.7013.
# 
# @GPL_HEADER_END


# numpy 1.1.1 has some kind of subtle bug in it that is tickled by
# rave's special handling of __import__. To work around it, import
# numpy first. (Downside is we may import numpy even if it isn't
# needed. Ah, well.)
try:
    import numpy
except ImportError:
    pass

import logging
logging.raiseExceptions = 1
import logging.config

#import getopt
import pdb
import os
import errno
import sys
import traceback
import threading
import signal
import time
import re

import rave.log as rlog
import rave.util.options as ropt
import rave.exceptions as rexcept
import rave.plugins.names as rnames
import rave.plugins.decorators as rdeco
import rave.cache.core as rccore
import rave.threads as rthreads
import rave.web as rweb
import rave.util.daemonize as rdaemon


get_log = rlog.log_factory("rave.raved")

opts = [
      [None, 'listen-port', "portnum", int ,
       'Port on which to listen for requests']
    , [None, 'listen-addr', "addr", str,
       'IP on which to listen to requests (default: localhost)']
    , [None, 'output-dir', "path", str ,
       'Where to store visualizations. May differ from data-dir.']
    , [None, 'data-dir', "path", str ,
       'Where to store working data. May differ from output-dir.']
    , [None, 'url-base', "string", str ,
       'Result URL prefix (default: http://localhost/)']
    , [None, 'log-config', "filename", str ,
       'Logging configuration file location']
    , [None, 'working-dir', "path", str ,
       'Current working directory of daemon process (default: $PWD)']
    , [None, 'namespace-config', "filename", str ,
       'Location of namespace configuration file']
    , [None, 'namespace-root', "path", str ,
       'Optional root of all namespace paths']
    , [None, 'vis-expire-after', "seconds", int,
       'Ultimate visualizations expire time (default: 5 minutes)']
    , [None, 'data-expire-after', "seconds", int,
       "Ultimate working data expire time (default: 5 minutes)"]
    , [None, 'data-purge-every', "seconds", int,
       "Time between expired data purges (default: 2 hours)"]
    , [None, 'vis-purge-every', "seconds", int,
       "Time between expired visualization purges (default: 2 hours)"]
    , [None, 'load-error-fatal', None, None ,
       'Stop if there are problems loading namespaces']
    , [None, 'no-daemon', None, None ,
       'Do not detach from terminal and run as a daemon']
    , [None, 'ana-threads', "num", int ,
       '# concurrently running analyses (default: 5)']
    , [None, 'threads', "num", int ,
       '# concurrently running analyses (default: 5)']
    , [None, 'pidfile', "filename", str ,
       "Where to write daemon's process ID (default: /var/run/raved.pid)"]
    , [None, 'stderr', "filename", str ,
       "Where to write STDERR for external processes. "
       "(default: /var/log/raved.err)"]
    , [None, 'stdout', "filename", str ,
       "Where to write STDOUT for external "
       "(default: /var/log/raved.err)"]
]

def parse_args(args):
    olist = ropt.OptionList.from_options([ropt.Option(*x) for x in opts])
    return olist, olist.parse(args)

def panic(msg, t, v, tb):
    try:
        f = open('/tmp/rave.err.%d' % os.getpid(), 'w')
        try:
            f.write("%s:\n" % msg)
            traceback.print_exception(
                etype=t, value=v, tb=tb, file=f)
        finally:
            f.close()
    except:
        pass
    sys.exit(1)
        

def print_namespaces(nsdict):
    get_log().info("%d available namespaces:", len(nsdict))
    for name, syms in nsdict.items():
        get_log().info("Actions in %s:", name)
        for sym in syms:
            get_log().info("\t%s/%s", name, sym)

def setup():
    try:
        scheduler = rthreads.Scheduler(args['ana-threads'])
        try:
            rdeco.ana_cache = rccore.Cache(
                  repository=args['data-dir']
                , scheduler=scheduler
                , purge_every=args['data-purge-every']
                , strat=rdeco.duration_strategy(args['data-expire-after'])
            )
            rdeco.fname_cache = rccore.Cache(
                  repository=args['output-dir']
                , scheduler=scheduler
                , purge_every=args['vis-purge-every']
                , strat=rdeco.duration_strategy(args['vis-expire-after'])
            )
            num_loaded, exceptions = rnames.init(
                args['namespace-config'], args['namespace-root'])
            for ns, e in exceptions.items():
                get_log().warn("error loading namespace %s", ns, exc_info=e)
            webserver = rweb.WebServerThread(
                rweb.WebServer(
                      scheduler
                    , args['url-base']
                    , args['listen-port']
                    , args['listen-addr']
                )
            )
        except:
            scheduler.stop()
            raise
        return scheduler, webserver
    except rexcept.ConfigurationError:
        raise
    except:
        get_log().exception("Error in setup")
        raise


def kick_off_threads(scheduler, webserver):
    scheduler.start()
    try:
        webserver.start()
    except:
        try: scheduler.stop()
        except: get_log().exception("Problem stopping worker threads")
        raise

def wrap_up_threads(scheduler, webserver):
    try: webserver.shutdown()
    except: get_log().exception("Problem stopping HTTP server")

    try: scheduler.stop()
    except: get_log().exception("Problem stopping worker threads")


def daemonize():
    if args.has_key('no-daemon'):
        return
    errfile = "%s/rave.err" % args["working-dir"]
    rdaemon.daemonize(
          wd     = args["working-dir"]
        , stdout = args["stdout"]
        , stderr = args["stderr"]
    )

def write_pid(pidfile):
    f = open(pidfile, 'w')
    try:
        print >>f, os.getpid()
    finally:
        f.close()
    os.chmod(pidfile, 0600)
        

def sanitize_args(args):
#   default settings
    args.setdefault("pidfile", "/var/run/raved.pid")
    args.setdefault("stderr", "/var/log/raved.err")
    args.setdefault("stdout", "/var/log/raved.err")
    args.setdefault("working-dir", os.getcwd())
    args.setdefault("vis-expire-after", 300)          # == 5 minutes
    args.setdefault("data-expire-after", 300)
    args.setdefault("namespace-root", "/")
    args.setdefault("url-base", "http://localhost/")
    args.setdefault("listen-addr", "localhost")
    args.setdefault("data-purge-every", 2 * 60 * 60)
    args.setdefault("vis-purge-every", 2 * 60 * 60)

#   ana-threads and threads are synonyms. If both are specified,
#   use ana-threads (and gripe)
    if args.has_key('ana-threads'):
        if args.has_key('threads'):
            print >>sys.stderr, (
                "Warning: 'ana-threads' and 'threads' "
                "are synonyms. Using value of 'ana-threads.'"
            )
            args['ana-threads'] = args['threads'] = args['ana-threads']
        else:
            args['threads'] = args['ana-threads']
    else:
    #   Set ana-threads from threads, or, failing that, default to 5
        args.setdefault('ana-threads'
            , args.setdefault('threads', 5)
        )

#   Required arguments
    def require(key):
        if not args.has_key(key):
            raise rexcept.ParameterError(
                "'%s' is a required argument" % key
            )
    require('output-dir')
    require('data-dir')
    require('log-config')

                

#   Rewrite log-config entry to refer to absolute pathname,
#   so it doesn't get fubared when/if we chdir in daemonize...
    if args.has_key('log-config'):
        if not args["log-config"].startswith("/"):
            args["log-config"] = "%s/%s" % (
                args["working-dir"], args["log-config"]
            )
            
    return args


def configure_signals(stop_event):
    def stop_server(signal, interrupted):
        log = get_log("stop_server")
        log.info("raved shutting down")
        stop_event.set()
    #   Make token effort to remove pidfile
        try:
            os.remove(args["pidfile"])
        except:
            pass

    signal.signal(signal.SIGTERM, stop_server)
    signal.signal(signal.SIGINT, stop_server)

def print_help(olist):
    print >>sys.stdout, ("RAVE: The Retrospective Analysis and "
                         "Visualization Engine")
    print >>sys.stdout, "Copyright (c) 2008 Carnegie Mellon University"
    print >>sys.stdout, ""
    print >>sys.stdout, olist.help_str()

def print_args():
    global args
    get_log().info("Using the following parameters:")
    for k, v in sorted(args.items()):
        get_log().info("\t%s: %s", k, str(v))

def init_app(stop_event):
    """
    Perform administrivia related to starting an application ---
    argument parsing, logging configuration, etc.
    """
    global args
    olist, args = parse_args(sys.argv[1:])
    try:
        args = sanitize_args(args)
    except rexcept.ParameterError, e:
        print >>sys.stderr, "Error: %s" % e
        print_help(olist)
        sys.exit(1)

    if args.has_key('help'):
        print_help(olist)
        sys.exit()
    if args.has_key('log-config'):
        print >>sys.stdout \
            , "Sending output to log, as configured in %s " \
              "(log config and init errors in /tmp/rave.err.<pid>)" % (
                   args["log-config"]
              )
    try:
        daemonize()
    except:
        t, v, tb = sys.exc_info()
        panic("Error detaching from terminal", t, v, tb)

    # Note: after this point, we've closed all the filehandles we
    # originally had. So log configuration has to be deferred until now,
    # or else the log filehandle would just get closed.

    try:
        logging.config.fileConfig(args['log-config'])
    except:
        t, v, tb = sys.exc_info()
        panic("Couldn't configure logging", t, v, tb)


    get_log().debug("Logging configured")
    get_log().info("Logging STDOUT at %s", args['stdout'])
    get_log().info("Logging STDERR at %s", args['stderr'])
    print >>sys.stdout, "raved started %s\n" % time.asctime()
    print >>sys.stderr, "raved started %s (stderr)" % time.asctime()

#   write_pid also has to happen after daemonizing, so we get the, uh,
#   right PID :)
    write_pid(args["pidfile"])
    configure_signals(stop_event)

def abnormal_exit(msg):
    if sys.stderr:
        print >>sys.stderr, msg
        print >>sys.stderr, "Check log for more informtion"
        print >>sys.stderr, "raved exiting abnormally"
    sys.exit(1)

def main():
    try:
        should_stop = threading.Event()
        init_app(should_stop)

        print_args()

        scheduler, webserver = setup()
        print_namespaces(rnames.exported_names())
        kick_off_threads(scheduler, webserver)

    #   Hang around until doomsday
    #   (As long as we're wait()ing, we wont catch any signals, so
    #   un-wait() occasionally, checking to see if we awoke because the
    #   event was set or because the timeout expired.)
        try:
            try:
                while True:
                    should_stop.wait(.1)
                    if should_stop.isSet():
                        break
            except:
                get_log().warn("Error in main loop -- shutting down")
                raise
        finally:
            try:
                wrap_up_threads(scheduler, webserver)
            except:
                get_log().exception("Problem cleaning up")
    except rexcept.ConfigurationError:
        t, v, tb = sys.exc_info()
        #pdb.set_trace()
        get_log().error("Configuration error: %s", v)
        abnormal_exit("Configuration error: %s" % v)
    except SystemExit:
        raise
    except:
        get_log().exception("Exception at toplevel")
        t, v, tb = sys.exc_info()
        abnormal_exit("%s: %s" % (t, v))


#   TODO: I have a feeling this should be sys.exit()....
    sys.exit(0)



if __name__ == '__main__':
    main()
