#!/usr/bin/python

import os
import re
import imp
import sys
import glob
import subprocess
import logging

from optparse import OptionParser

re_ignore = re.compile(r'(~$|^_|\.(dpkg-(old|dist|new|tmp)|example)$|\.pyc|\.comc$)')

def main():
    parser = OptionParser("usage: %prog [options] start|stop|reload [configs]")

    parser.add_option('--conf-dir', dest='conf_dir',
        default='/etc/gunicorn.d')
    parser.add_option('--pid-dir', dest='pid_dir',
        default='/var/run/gunicorn')
    parser.add_option('--log-dir', dest='log_dir',
        default='/var/log/gunicorn')

    parser.add_option('-v', '--verbosity', type='int', dest='verbosity',
        default=2, help='Verbosity level; 0=minimal output, 1=normal output' \
            ' 2=verbose output, 3=very verbose output')

    options, args = parser.parse_args()

    try:
        logging.basicConfig(
            format='%(levelname).1s: %(message)s',
            level={
                0: logging.ERROR,
                1: logging.WARNING,
                2: logging.INFO,
                3: logging.DEBUG,
            }[options.verbosity]
        )
    except KeyError:
        parser.error("Invalid verbosity level")

    log = logging.getLogger('gunicorn-debian')

    configs = args[1:]

    try:
        action = args[0]
    except IndexError:
        parser.error("Missing action")

    if action not in ('start', 'stop', 'restart', 'reload'):
        parser.error("Invalid action: %s" % action)

    files = glob.glob(os.path.join(options.conf_dir, '*'))

    if not os.path.exists(options.pid_dir):
        log.info("Creating %s", options.pid_dir)
        os.makedirs(options.pid_dir)

    # Update Python path so configurations can extend each other.
    sys.path.append(options.conf_dir)

    sys.dont_write_bytecode = True

    for filename in sorted(files):
        basename = os.path.basename(filename)

        if re_ignore.search(basename):
            log.debug("Ignoring %s", filename)
            continue

        if configs and basename not in configs:
            log.debug("Skipping %s", filename)
            continue

        log.debug("Loading %s", filename)

        if sys.version_info > (2, 6):
            # We are using a version that understands PYTHONDONTWRITEBYTECODE
            # so it is safe to use imp.load_source here - otherwise we create
            # "pyc"-like files in /etc/gunicorn.d which we then try and parse.
            module = imp.load_source(filename, filename)
            CONFIG = getattr(module, 'CONFIG', None)
        else:
            module = {}
            execfile(filename, module)
            CONFIG = module.get('CONFIG')

        if not CONFIG:
            continue

        config = Config(filename, options, CONFIG, log)

        if options.verbosity < 3:
            config.print_name()

        log.debug("Calling .%s() on %s", action, config.basename())
        getattr(config, action)()

    # Kill any renaming pidfiles to prevent the case where we remove or
    # renaming a configuration and it doesn't get stopped or restarted.
    if action == 'stop' and not configs:
        for pidfile in glob.glob(os.path.join(options.pid_dir, '*.pid')):
            log.debug("Stopping extraenous pidfile %s", pidfile)
            subprocess.call((
                'start-stop-daemon',
                '--stop',
                '--oknodo',
                '--retry', '1',
                '--quiet',
                '--pidfile', pidfile,
            ))

            try:
                os.unlink(pidfile)
            except OSError:
                pass

    return 0

class Config(dict):
    def __init__(self, filename, options, data, log):
        self.filename = filename
        self.options = options
        self.log = log

        data['args'] = list(data.get('args', []))
        data.setdefault('mode', 'wsgi')
        data.setdefault('user', 'www-data')
        data.setdefault('group', 'www-data')
        data.setdefault('retry', '60')
        data.setdefault('environment', {})
        data.setdefault('working_dir', '/')
        data.setdefault('python', '/usr/bin/python')

        self.update(data)

        assert self['mode'] in ('wsgi', 'django', 'paster')

    def print_name(self):
        sys.stdout.write(" [%s]" % self.basename())
        sys.stdout.flush()

    def basename(self):
        return os.path.basename(self.filename)

    def pidfile(self):
        return os.path.join(self.options.pid_dir, '%s.pid' % self.basename())

    def logfile(self):
        return os.path.join(self.options.log_dir, '%s.log' % self.basename())

    def check_call(self, *args, **kwargs):
        self.log.debug("Calling subprocess.check_call(*%r, **%r)", args, kwargs)
        subprocess.check_call(*args, **kwargs)

    def start(self):
        daemon = {
            'wsgi': '/usr/bin/gunicorn',
            'django': '/usr/bin/gunicorn_django',
            'paster': '/usr/bin/gunicorn_paster',
        }[self['mode']]

        args = [
            'start-stop-daemon',
            '--start',
            '--oknodo',
            '--quiet',
            '--chdir', self['working_dir'],
            '--pidfile', self.pidfile(),
            '--exec', self['python'], '--', daemon,
        ]

        gunicorn_args = [
            '--pid', self.pidfile(),
            '--name', self.basename(),
            '--user', self['user'],
            '--group', self['group'],
            '--daemon',
            '--log-file', self.logfile(),
        ]

        env = os.environ.copy()
        env.update(self['environment'])

        self.check_call(args + gunicorn_args + self['args'], env=env)

    def stop(self):
        self.check_call((
            'start-stop-daemon',
            '--stop',
            '--oknodo',
            '--quiet',
            '--retry', self['retry'],
            '--pidfile', self.pidfile(),
        ))

    def restart(self):
        self.stop()
        self.start()

    def reload(self):
        try:
            self.check_call((
                'start-stop-daemon',
                '--stop',
                '--signal', 'HUP',
                '--quiet',
                '--pidfile', self.pidfile(),
            ))
        except subprocess.CalledProcessError:
            self.log.debug("Could not reload, so restarting instead")
            self.restart()

if __name__ == '__main__':
    sys.exit(main())
