#!/usr/bin/python
from optparse import OptionParser
from time import sleep
import subprocess
import os
import sys
import signal
import traceback
from datetime import datetime, timedelta
import syslog

home = os.environ.get("CUMIN_HOME", os.path.normpath("/usr/share/cumin"))
sys.path.append(os.path.join(home, "python"))

from cumin.config import *
from cumin.errors import CuminErrors

from parsley.loggingex import *
log = logging.getLogger("cumin.master")

def call_sys_exit(sig,frame):
    sys.exit()

def set_exit_handler(func):
    signal.signal(signal.SIGTERM, func)

def log_parse_errors(r):
    msg = os.fdopen(r, "r").readlines()
    if len(msg) > 0:
        _syslog.log()
        log.error("".join(msg))
        return 1
    return 0

def get_args(app, section, init_only, console, extra_options=""):
    args = [app, "--section="+section.strip()]
    if init_only:
        args.append("--init-only")
    if not console:
        args.append("--daemon")
    if len(extra_options) != 0:
        args += extra_options.split(" ")
    prog_string = "".join([" "+x for x in args])
    return args, prog_string

class _syslog(object):
    enabled = False
    @classmethod
    def log(cls):
        if _syslog.enabled:
            home = os.environ.get("CUMIN_HOME", os.path.normpath("/usr/share/cumin"))
            syslog.syslog("cumin: master script exited with errors, see %s/log/master.log" % home)

def main():
    
    # tuple indices, for clarity
    PROCESS = 0
    ARGS = 1
    PROG_STRING = 2

    config = CuminMasterConfig()

    # Trap exit from parser and save standard error for logging
    # Then put stderr back to original value
    r, w = os.pipe()
    sys.stderr = os.fdopen(w,"w")
    try:
        values = config.parse().master

        parser = OptionParser()

        parser.add_option("--init-only", dest="init_only", action="store_true", default=False,
                          help="Check options and initialization, then return.")

        parser.add_option("--webs", dest="webs", default=values.webs,
                          help="Configuration section names for cumin-web instances."\
                               "\nEach value implies a separate cumin-web instance.")

        parser.add_option("--datas", dest="datas", default=values.datas,
                          help="Configuration section names for cumin-data instances."\
                               "\nEach value implies a separate cumin-data instance.")

        parser.add_option("--console", dest="console", action="store_true", default=False,
                          help="Log to stderr rather than master.log, no IO redirection for children.")

        parser.add_option("--data-options", dest="data_options", default="", type=str,
                          help="Additional options string to pass to data instances."\
                              "\nEnclose in quotes, options must be --option form, splits on spaces."\
                               '\nExample: data_options="--print-events=5 --print-stats"')

        parser.add_option("--web-options", dest="web_options", default="", type=str,
                          help="Additional options string to pass to web instances."\
                               "\nEnclose in quotes, options must be --option form, splits on spaces."\
                               '\nExample: web_options="--debug --port=12345"')
        parser.add_option("--syslog", dest="syslog", action="store_true", default=False,
                          help="Log general error notfications to syslog.  Intended for systemd")

        (options, args) = parser.parse_args()
    except SystemExit:
        options = args = None
    except:
        options = args = None
        traceback.print_exc()
    sys.stderr.close()
    sys.stderr = sys.__stderr__

    # Parse may have failed, in which case make a quick check for options ourselves
    if options:
        _syslog.enabled = options.syslog
        console = options.console
    else:
        _syslog.enabled = "--syslog" in sys.argv[1:]
        console = "--console" in sys.argv[1:]
    if console:
        log_dest = sys.stderr
    else:
        log_dest =  os.path.join(home, "log", "master.log")  
    enable_logging("cumin.master", logging.INFO, log_dest)


    # Parser exited, either on --help or with errors                   
    if not options:
        return log_parse_errors(r)
    
    if len(args) != 0:
        log.error("Extra arguments:" + "".join([" "+arg for arg in args]))
        _syslog.log()
        return 1

    # Get our list of cumin-web and data instances
    # create list elements to hold the process object, section arg, and app
    apps = []
    if len(options.webs) > 0:
        for instance in options.webs.split(','):
            args, prog_string = get_args("cumin-web", instance, options.init_only, console, options.web_options)
            apps.append([None, args, prog_string])

    if len(options.datas) > 0:
        for instance in options.datas.split(','):
            args, prog_string = get_args("cumin-data", instance, options.init_only, console, options.data_options)
            apps.append([None, args, prog_string])
    
    # Launch and babysit, do not restart if options.init_only is set
    complete = 0
    return_code = 0
    sleep_time = 0.25
    slow_down = 40 # slow down polling after initial period
    try:
        def start(app, verb):
            app[PROCESS] = subprocess.Popen(app[ARGS])
            if app[PROCESS]:
                log.info(verb+"ed subprocess (pid %s): %s" %\
                         (app[PROCESS].pid, app[PROG_STRING]))
            else:
                log.warn("Failed to %s: %s" % (verb, app[PROG_STRING]))

        for app in apps:
            start(app, "Start")

        while complete != len(apps):
            sleep(sleep_time)
            if slow_down > 0:
                slow_down -= 1
                if slow_down == 0:
                    sleep_time = 5

            for app in apps:
                poll = app[PROCESS] and app[PROCESS].poll()
                if poll is not None:

                    # If the low bit is set on the return code, the 
                    # process got an error during init checks.  
                    # Exit and shut down any processes that have already
                    # been started, do not start the remaining.
                    # Note, signals that cause termination will result in
                    # a negative error code, we treat those as "normal"
                    err = CuminErrors.translate(poll)
                    if poll > 0 and poll & 1:
                        log.error("Subprocess (%s) failed init checks "\
                                  "with status %s (%s), %s"\
                                  % (app[PROCESS].pid, poll, err[0], err[1]))
                        log.info("Subprocess logs may contain more details.")
                        log.info("Stopping cumin")
                        _syslog.log()
                        app[PROCESS] = None
                        return_code = 2
                        complete = len(apps)
                        break
                    else:
                        log.warn("Subprocess (%s) exited with status %s (%s), %s"\
                                 % (app[PROCESS].pid, poll, err[0], err[1]))
                        if poll != 0:
                            log.info("Subprocess logs may contain more details.")
                        if options.init_only:
                            app[PROCESS] = None
                            complete += 1
                        else:
                            start(app, "Restart")
    finally:
        # Try a ctrl-C first
        log.info("Send SIGINT to all children")
        complete = 0
        for app in apps:            
            if app[PROCESS]:
                complete += 1
                os.kill(app[PROCESS].pid, signal.SIGINT)

        # Give children 10 seconds to exit, then bail
        then = datetime.now()
        while complete != 0:        
            for app in apps:
                poll = app[PROCESS] and app[PROCESS].poll()
                if poll is not None:
                    app[PROCESS] = None
                    complete -= 1

            if complete == 0:
                log.info("All children exited")
                break
        
            sleep(0.25)
            if datetime.now() - then > timedelta(seconds=10):
                for app in apps:  # just to be paranoid 
                    app[PROCESS] and os.kill(app[PROCESS].pid, signal.SIGKILL)
                log.info("Timed out waiting for children, exited")
                break

    return return_code

if __name__ == "__main__":
    # This is necessary so that on a SIGTERM we can call sys.exit()
    # and cause the finally block to be executed.  Ctrl-C will
    # run the finally block anyway.
    set_exit_handler(call_sys_exit)
    try:
        sys.exit(main())
    except KeyboardInterrupt:
        sys.exit(0)

