#!/usr/bin/python
#
# -*- coding: utf-8; -*-
#
# (c) 2007-2008 Mandriva, http://www.mandriva.com/
#
# $Id$
#
# This file is part of Pulse 2, http://pulse2.mandriva.org
#
# Pulse 2 is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# Pulse 2 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Pulse 2; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.

# Big modules
import os
import sys
import optparse
import time

from twisted.internet import epollreactor
epollreactor.install()
from twisted.internet import reactor
from twisted.internet.defer import maybeDeferred
from twisted.internet.task import LoopingCall, deferLater


# Filter SA warns to prevent trivial (hex/dec notation) error printing on STDOUT
import warnings
warnings.filterwarnings("ignore", category=FutureWarning)

from mmc.site import mmcconfdir

from pulse2.version import getVersion, getRevision
from pulse2.scheduler.config import SchedulerConfig, SchedulerDatabaseConfig
from pulse2.database.msc import MscDatabase

from pulse2.scheduler.control import MscDispatcher
from pulse2.scheduler.phases import installed_phases
from pulse2.scheduler.utils import SpawnProxy
from pulse2.scheduler.gateway import SchedulerGatewayFactory


import logging
import logging.config


class SchedulerSetupError(Exception):
    def __init__(self, msg):
        self.msg = msg

    def __repr__(self):
        return "Scheduler setup failed: %s" % self.msg

class App :
    listen_unix = None
    socket_file = None
    proxy_pid = None

    # mainloop caller
    loop = None

    def __init__(self, config):
        self.config = config
        self.logger = logging.getLogger()
        self.socket_file = self.config.scheduler_proxy_socket_path

        reactor.addSystemEventTrigger("before",
                                      "shutdown",
                                       self.clean_up)

    def setup(self):
        d = maybeDeferred(self.start_unix_socket)
        d.addCallback(self.initialize_dispatcher)
        d.addErrback(self.eb_setup)
        return d

    def eb_setup(self, failure):
        self.logger.error("scheduler setup failed: %s" % str(failure))
        reactor.stop()
        return failure

    def start(self):
        d = self.setup()
        d.addCallback(self.spawn_proxy)
        d.addCallback(self._schedule_dispatcher)
        d.addErrback(self.eb_start)
        return d

    def _schedule_dispatcher(self, reason):
        return deferLater(reactor,
                          self.config.initial_wait,
                          self.schedule_dispatcher)

    def eb_start(self, failure):
        self.logger.error("scheduler start failed: %s" % str(failure))
        reactor.stop()
        return failure



    def start_unix_socket(self):
        """
        Creates a communication channel between scheduler and scheduler-proxy.
        """
        try:
            gateway = SchedulerGatewayFactory()
            if os.path.exists(self.socket_file):
                self.logger.info("Unix socket: unlinking old file")
                os.unlink(self.socket_file)
            self.listen_unix = reactor.listenUNIX(self.socket_file, gateway)

        except Exception, e:
            self.logger.error("Unix socket start failed: %s" % str(e))
            return False
        return True

    def initialize_dispatcher(self, reason):
        MscDispatcher().installed_phases = installed_phases

        d = maybeDeferred(MscDispatcher().initialize, self.config)
        @d.addErrback
        def _eb(failure):
            self.logger.error("Dispatcher init error: %s" % str(failure))
            reactor.stop()
        return d

    def schedule_dispatcher(self):
        """ """
        self.logger.info("Starting the main dispatcher ...")
        self.loop = LoopingCall(MscDispatcher().mainloop)
        d = self.loop.start(self.config.awake_time, now=True)
        d.addErrback(self.eb_loop)
        return d

    def eb_loop(self, failure):
        self.logger.error("Main loop runtime failed: %s" % str(failure))

    def spawn_proxy(self, reason):
        """ Starts the scheduler-proxy as a child process. """
        self.logger.info('Starting of XMLRPC Proxy')
        path = self.config.scheduler_proxy_path
        try:
            self.proxy_process = SpawnProxy(path)
            self.proxy_process.run()

            self.proxy_pid = self.proxy_process.protocol.transport.pid
            self.logger.info('XMLRPC Proxy pid=%s' % (self.proxy_pid))

            return True
        except OSError, e:
            self.logger.error("Start of XMLRPC Proxy failed: %s" % str(e))
            return False
        except Exception, e:
            self.logger.error("Start of XMLRPC Proxy failed: %s" % str(e))
            return False



    def clean_up(self):
        """ Cleanning up routine called before the shutdown. """
        self.logger.info('scheduler %s: Shutting down and cleaning up' % (self.config.name))

        # dispatcher invalidate
        MscDispatcher().cancel()
        # mainloop invalidate
        self.loop.stop()

        # attempt to stop the unix socket listening
        if self.listen_unix :
            d = self.listen_unix.stopListening()
            @d.addCallback
            def cb(reason):
                self.logger.info("Unix socket: stop listening succeed")
                if os.path.exists(self.socket_file):
                    self.logger.info("Unix socket: unlinking the file")
                    os.unlink(self.socket_file)
            # attempt to stop the scheduler-proxy
            d.addCallback(self.close_proxy)

            @d.addCallback
            def final(reason):
                self.logger.info('scheduler %s: End' % (self.config.name))

            @d.addErrback
            def eb(failure):
                self.logger.error("Unix socket: stop listening error: %s" % failure)
            return d


    def close_proxy(self, reason):
        """ Attempt to stop the scheduler-proxy as a child process. """
        if os.path.exists(os.path.join("/","proc", str(self.proxy_pid))) :
            self.logger.info('Terminating the XMLRPC Proxy')
            self.proxy_process.protocol.transport.loseConnection()


def get_next_delay(base):
    ret = base                  # next delay in "base" seconds,
    ret -= time.time() % base   # rounded to the lower (second modulo base)
    return ret



def startService(config):
    logger = logging.getLogger()
    if not config.username:
        logger.warn('scheduler %s: no username set !!' % (config.name))
    if not config.password:
        logger.warn('scheduler %s: no password set !!' % (config.name))
    # check versus MySQLdb version
    import MySQLdb
    (v1, v2, v3, v4, v5) = MySQLdb.version_info
    force_ascii = False
    warn_debian = False
    if v1 == 1: # handle v. 1.x
        if v2 <= 1: # handle of v. 1.0.x and 1.1.x
            force_ascii = True
            warn_debian = True
        elif v2 == 2: # handle of v. 1.2.x
            if v3 == 0: # handling of v. 1.2.0.x
                force_ascii = True
                warn_debian = True
            if v3 == 1: # handling of v. 1.2.1.x
                warn_debian = True
                if v4 != 'final': # versions up to 1.2.1c??? are buggy => inject using ascii convertion
                    force_ascii = True
            if v3 == 2: # handling of v. 1.2.2.x
                warn_debian = True

    if force_ascii :
        logger.warn('scheduler "%s": python-mysqldb too old (spotted %s), using "ascii" as db encoding' % (config.name, MySQLdb.version_info))
        config.dbencoding = 'ascii'

    if warn_debian :
        import platform
        try :
            (p,v,i) = platform.dist()
            if p == 'debian' :
                logger.warn('scheduler "%s": Please make sure that your python-mysql package is at least 1.2.2-7; on Debian-based platforms previous versions are buggy (broken auto-reconnect), see http://packages.debian.org/changelogs/pool/main/p/python-mysqldb/python-mysqldb_1.2.2-7/changelog#versionversion1.2.2-7' % (config.name))
        except :
            pass

    launchers = map(lambda(a): 'xml://%s:%s' % (config.launchers[a]['host'], config.launchers[a]['port']), config.launchers)
    logger.info('scheduler %s: available launchers: %s' % (config.name, ' '.join(launchers)))


    pref_ntw_display = [(ip +"/"+ mask) for ip, mask in config.preferred_network]
    logger.info("Preferred network is set to %s" % str(pref_ntw_display))


    App(config).start()

    logger.info('scheduler %s: setting threadpool max size to %d' % (config.name, config.max_threads))
    reactor.suggestThreadPoolSize(config.max_threads)

    logger.info('scheduler %s: listening on %s:%d' % (config.name, config.host, config.port))
    reactor.run()
    return 0


def main():
    parser = optparse.OptionParser()

    parser.add_option("-c",
                      "--config-file",
                      help='path to the config file',
                      default=mmcconfdir + '/pulse2/scheduler/scheduler.ini')

    parser.add_option("-d", "--debug",
            dest="debug",
            action="store_true",
            default=False,
            help='verbose debug mode')
    (options, args) = parser.parse_args()

    if not os.path.exists(options.config_file):
        print "Config file '%s' does not exist." % options.config_file
        sys.exit(3)

    # start logger
    logging.config.fileConfig(options.config_file)
    logger = logging.getLogger()
    logger.info("Scheduler version('%s') build('%s')" % (str(getVersion()), str(getRevision())))

    if options.debug :
        from mmc.core.log import ColoredFormatter
        hdlr2 = logging.StreamHandler()
        hdlr2.setFormatter(ColoredFormatter("%(levelname)-18s %(message)s"))
        logger.addHandler(hdlr2)

    # parse conf
    config = SchedulerConfig()
    logger.info("Reading configuration file: %s" % options.config_file)
    try:
        config.setup(options.config_file)
    except Exception, e:
        logger.error(e)
        logger.error("Please fix the configuration file")
        sys.exit(1)

    try:
        confmsc = SchedulerDatabaseConfig()
        confmsc.setup(options.config_file)
        if not MscDatabase().activate(confmsc): # does the db_check
            sys.exit(1)
    except Exception, e:
        logger.error(e)
        logger.error("Please fix the configuration file")
        sys.exit(1)


    # start service
    sys.exit(startService(config))

if __name__ == '__main__':
    main()
