#!/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.

"""
    This is the Pulse2 launchers manager
    The goal is to provide a helper to run launchers:
    * start (fork)
    * stop (kill)
"""

# Big modules
import os
import os.path
import sys
import logging
import logging.config
import optparse
import signal
import time
import errno

time_to_wait = 2 # first stage: check of it is alive / dead after having being run /stopped for time_to_wait seconds
check_count = 5 # second stage: attempts to obtain a good result (check_count tries) to a XMLRPC call

from mmc.site import mmcconfdir
from pulse2.launcher.config import LauncherConfig

def start_launcher(name, config):
    """
    daemonize a launcher
    """
    logger = logging.getLogger()

    logger.info('daemonization started: launcher "%s"' % name)

    pid_path = config.pid_path
    if not os.path.isdir(pid_path):
        os.makedirs(pid_path)

    pid = get_pidfile(config, name)
    if pid:
        if pid_is_alive(pid):
            logger.error('failed to start launcher "%s": seems to be running under pid %d' % (name, pid))
            print 'failed to start launcher "%s": seems to be running under pid %d' % (name, pid)
            return False
        else:
            clear_pidfile(config, name)
            logger.error('while starting launcher "%s": clearing old pid file (pid was %d)' % (name, pid))

    # as a launcher itself do not fork(), fork is done here
    # do the UNIX double-fork magic, see Stevens' "Advanced
    # Programming in the UNIX Environment" for details (ISBN 0201563177)
    try:
        pid = os.fork()
        if pid > 0:
            # first parent stays alive
            # let's check if it is realy alive
            # check is done in two stages:

            # let's do the first stage
            time.sleep(time_to_wait)
            pid = get_pidfile(config, name)
            if not pid_is_alive(pid): # os.WNOHANG make os.waitpid() to return (0, 0) if child is alive
                logger.error('daemonization part #2 failed: launcher "%s" died in less than %d seconds' % (name, time_to_wait))
                clear_pidfile(config, name)
                return False

            # TODO: implement second stage here !
            return True
    except OSError, e:
        logger.error('daemonization part #1 failed: %d (%s)' % (e.errno, e.strerror))
        print >> sys.stderr, "daemonization part #1 failed: %d (%s)" % (e.errno, e.strerror)
        sys.exit(1)

    # decouple from parent environment
    os.close(sys.stdin.fileno())
    os.close(sys.stdout.fileno())
    os.close(sys.stderr.fileno())
    os.chdir("/")
    os.setsid()

    # do second fork
    try:
        pid = os.fork()
        if pid > 0:
            logger.info('daemonization succedeed: "%s", pid %d' % (name, pid) ) # exit from second parent, print eventual PID before
            set_pidfile(config, name, pid) # records the PID from here, as it is the only place where we know it
            sys.exit(0)
    except OSError, e:
        logger.error('daemonization part #2 failed: %d (%s)' % (e.errno, e.strerror))
        sys.exit(1)

    # drop privs
    os.seteuid(0)
    os.setegid(0)
    os.setgid(config.daemon_group)
    os.setuid(config.daemon_user)

    launcher_path = config.launcher_path
    cmd = [launcher_path, '--id', name]
    if config.launchers[name]['logconffile']:
        cmd.extend(['--log-config-file', config.launchers[name]['logconffile']])
    logger.info('daemonization ended: switching to %s' % cmd)
    os.execv(launcher_path, cmd)

def stop_launcher(name, config):
    logger = logging.getLogger()
    os.seteuid(0)
    os.setegid(0)

    pid = get_pidfile(config, name)
    if pid:
        if pid_is_alive(pid):
            logger.info('attempt to kill launcher "%s" (pid %d)' % (name, pid))
            kill_pid(pid, signal.SIGTERM) # TODO: check that kill was successful
            time.sleep(time_to_wait)
            if not pid_is_alive(pid): # os.WNOHANG make os.waitpid() to return (0, 0) if child is alive
                logger.info('launcher "%s" killed using SIGTERM' % name)
                clear_pidfile(config, name)
                return True
            else:
                logger.error('failed to kill launcher "%s" using SIGTERM' % name)
                return False
        else:
            logger.error('failed to stop launcher "%s": was supposed to be running under pid %d' % (name, pid))
            print 'failed to stop launcher "%s": was supposed to be running under pid %d' % (name, pid)
            clear_pidfile(config, name)
            return False
    else:
        logger.error('failed to stop launcher "%s": was not running' % (name))
        print 'failed to stop launcher "%s": was not running' % (name)
        clear_pidfile(config, name)
        return False

def clear_pidfile(config, name):
    os.setegid(0)
    os.seteuid(0)
    pid_file = os.path.join(config.pid_path, '%s.pid' % name)
    if os.path.isfile(pid_file):
        os.unlink(pid_file)
    os.setegid(config.daemon_group)
    os.seteuid(config.daemon_user)

def set_pidfile(config, name, pid):
    os.setegid(0)
    os.seteuid(0)
    pid_path = config.pid_path
    pid_file = os.path.join(config.pid_path, '%s.pid' % name)
    if not os.path.isdir(pid_path):
        os.makedirs(pid_path)
    os.system("echo %d > %s" % (pid, pid_file))
    os.setegid(config.daemon_group)
    os.seteuid(config.daemon_user)

def pid_is_alive(pid):
    return kill_pid(pid, 0)

def kill_pid(pid, signal):
    os.setegid(0)
    os.seteuid(0)
    try:
        os.kill(pid, signal)
    except OSError, err:
        if err.errno == errno.ESRCH:
            os.setegid(config.daemon_group)
            os.seteuid(config.daemon_user)
            return False
        elif err.errno == errno.EPERM: # WTF ? I *am* root, am I ?
            os.setegid(config.daemon_group)
            os.seteuid(config.daemon_user)
            return True
        else:
            os.setegid(config.daemon_group)
            os.seteuid(config.daemon_user)
            return False
    else:
        os.setegid(config.daemon_group)
        os.seteuid(config.daemon_user)
        return True

def get_pidfile(config, name):
    os.setegid(0)
    os.seteuid(0)
    pid_file = os.path.join(config.pid_path, '%s.pid' % name)
    if not os.path.isfile(pid_file):
        os.setegid(config.daemon_group)
        os.seteuid(config.daemon_user)
        return False
    f = open(pid_file)
    for line in f:
        try:
            ret = int(line)
            f.close()
            os.setegid(config.daemon_group)
            os.seteuid(config.daemon_user)
            return ret
        except ValueError:
            pass
    os.setegid(config.daemon_group)
    os.seteuid(config.daemon_user)
    return False

if __name__ == "__main__":
    parser = optparse.OptionParser()
    parser.add_option("-c", "--config-file", help='path to the config file', default=mmcconfdir + '/pulse2/launchers/launchers.ini')
    parser.add_option("-o", "--id", help='id of this launcher')
    (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(1)

    if len(args)!=1:
        print "command is missing"
        sys.exit(2)

    # we have to advance carefully here in order not to break the logging system
    # hypothesis: the logging folder already exists with proper permissions
    # instanciate object
    config = LauncherConfig()

    # gather right user and password
    config.presetup(options.config_file)

    # set umask and effective UID and GID values RIGHT NOW
    os.umask(config.umask)
    os.setegid(config.daemon_group)
    os.seteuid(config.daemon_user)

    # Try to create the default log directory
    if not os.path.exists(config.logdir):
        try:
            os.mkdir(config.logdir)
        except:
            # Maybe we are not root, so it may not be a problem if the mkdir
            # failed
            pass

    # we changed our identify a few lines above, log file will be created with proper uid/gid/umask
    logging.config.fileConfig(options.config_file)
    logger = logging.getLogger()

    # last step: parse conf
    config.setup(options.config_file)

    if args[0] == "start":
        logger.info('starting Pulse2 Launchers')
        print('Starting Pulse2 Launchers:')
        # parse sections to keep launchers-specifics
        for section in config.launchers:
            print('\t%s' % section)
            if start_launcher(section, config):
                logger.info('Launcher "%s" started' % section)
                print('OK')
            else:
                logger.warn('Failed to start launcher "%s"' % section)
                print('FAILED')
    elif args[0] == "stop":
        logger.info('stopping Pulse2 Launchers')
        print('Stopping Pulse2 Launchers:')
        for section in config.launchers:
            print('\t%s' % section)
            if stop_launcher(section, config):
                logger.info('Launcher "%s" started' % section)
                print('OK')
            else:
                logger.warn('Failed to stop launcher "%s"' % section)
                print('FAILED')
