"""

djsupervisor.management.commands.supervisor:  djsupervisor mangement command
----------------------------------------------------------------------------

This module defines the main management command for the djsupervisor app.
The "supervisor" command acts like a combination of the supervisord and
supervisorctl programs, allowing you to start up, shut down and manage all
of the proceses defined in your Django project.

The "supervisor" command suports several modes of operation:

    * called without arguments, it launches supervisord to spawn processes.

    * called with the single argument "getconfig", is prints the merged
      supervisord config to stdout.

    * called with the single argument "autoreload", it watches for changes
      to python modules and restarts all processes if things change.

    * called with any other arguments, it passes them on the supervisorctl.

"""

from __future__ import absolute_import, with_statement

import sys
import os
import time
from textwrap import dedent
import traceback
from ConfigParser import RawConfigParser, NoOptionError
try:
    from cStringIO import StringIO
except ImportError:
    from StringIO import StringIO

from supervisor import supervisord, supervisorctl

from django.core.management.base import BaseCommand, CommandError
from django.conf import settings

from djsupervisor.config import get_merged_config
from djsupervisor.events import CallbackModifiedHandler

AUTORELOAD_PATTERNS = getattr(settings, "SUPERVISOR_AUTORELOAD_PATTERNS",
                              ['*.py'])
AUTORELOAD_IGNORE = getattr(settings, "SUPERVISOR_AUTORELOAD_IGNORE_PATTERNS", 
                            [".*", "#*", "*~"])

class Command(BaseCommand):

    args = "[<command> [<process>, ...]]"

    help = dedent("""
           Manage processes with supervisord.

           With no arguments, this spawns the configured background processes.

           With a command argument it lets you control the running processes.
           Available commands include:

               supervisor getconfig
               supervisor shell
               supervisor start <progname>
               supervisor stop <progname>
               supervisor restart <progname>

           """).strip()

    def add_arguments(self, parser):
        parser.add_argument('ctl-command', nargs='*')
        parser.add_argument(
            "--daemonize",
            "-d",
            action="store_true",
            dest="daemonize",
            default=False,
            help="daemonize before launching subprocessess"
        )
        parser.add_argument(
            "--pidfile",
            action="store",
            dest="pidfile",
            help="store daemon PID in this file"
        )
        parser.add_argument(
            "--logfile",
            action="store",
            dest="logfile",
            help="write logging output to this file"
        )
        parser.add_argument(
            "--project-dir",
            action="store",
            dest="project_dir",
            help="the root directory for the django project"
                 " (by default this is guessed from the location"
                 " of manage.py)"
        )
        parser.add_argument(
            "--config-file",
            action="store",
            dest="config_file",
            help="the supervisord configuration file to load"
                 " (by default this is <project-dir>/supervisord.conf)"
        )
        parser.add_argument(
            "--launch",
            "-l",
            metavar="PROG",
            action="append",
            dest="launch",
            help="launch program automatically at supervisor startup"
        )
        parser.add_argument(
            "--nolaunch",
            "-n",
            metavar="PROG",
            action="append",
            dest="nolaunch",
            help="don't launch program automatically at supervisor startup"
        )
        parser.add_argument(
            "--exclude",
            "-x",
            metavar="PROG",
            action="append",
            dest="exclude",
            help="exclude program from supervisor config"
        )
        parser.add_argument(
            "--include",
            "-i",
            metavar="PROG",
            action="append",
            dest="include",
            help="don't exclude program from supervisor config"
        )
        parser.add_argument(
            "--autoreload",
            "-r",
            metavar="PROG",
            action="append",
            dest="autoreload",
            help="restart program automatically when code files change"
                 " (debug mode only;"
                 " if not set then all programs are autoreloaded)"
        )
        parser.add_argument(
            "--noreload",
            action="store_true",
            dest="noreload",
            help="don't restart processes when code files change"
        )

    def run_from_argv(self,argv):
        #  Customize option handling so that it doesn't choke on any
        #  options that are being passed straight on to supervisorctl.
        #  Basically, we insert "--" before the supervisorctl command.
        #
        #  For example, automatically turn this:
        #      manage.py supervisor -l celeryd tail -f celeryd
        #  Into this:
        #      manage.py supervisor -l celeryd -- tail -f celeryd
        #
        i = 2
        while i < len(argv):
            arg = argv[i]
            if arg == "--":
                break
            elif arg.startswith("--"):
                i += 1
            elif arg.startswith("-"):
                i += 2
            else:
                argv = argv[:i] + ["--"] + argv[i:]
                break
        return super(Command,self).run_from_argv(argv)

    def handle(self, *args, **options):
        args = args or tuple(options.pop('ctl-command'))

        #  We basically just construct the merged supervisord.conf file
        #  and forward it on to either supervisord or supervisorctl.
        #  Due to some very nice engineering on behalf of supervisord authors,
        #  you can pass it a StringIO instance for the "-c" command-line
        #  option.  Saves us having to write the config to a tempfile.
        cfg_file = OnDemandStringIO(get_merged_config, **options)
        #  With no arguments, we launch the processes under supervisord.
        if not args:
            return supervisord.main(("-c",cfg_file))
        #  With arguments, the first arg specifies the sub-command
        #  Some commands we implement ourself with _handle_<command>.
        #  The rest we just pass on to supervisorctl.
        if not args[0].isalnum():
            raise ValueError("Unknown supervisor command: %s" % (args[0],))
        methname = "_handle_%s" % (args[0],)
        try:
            method = getattr(self,methname)
        except AttributeError:
            return supervisorctl.main(("-c",cfg_file) + args)
        else:
            return method(cfg_file,*args[1:],**options)

    #
    #  The following methods implement custom sub-commands.
    #

    def _handle_shell(self,cfg_file,*args,**options):
        """Command 'supervisord shell' runs the interactive command shell."""
        args = ("--interactive",) + args
        return supervisorctl.main(("-c",cfg_file) + args)

    def _handle_getconfig(self,cfg_file,*args,**options):
        """Command 'supervisor getconfig' prints merged config to stdout."""
        if args:
            raise CommandError("supervisor getconfig takes no arguments")
        print cfg_file.read()
        return 0

    def _handle_autoreload(self,cfg_file,*args,**options):
        """Command 'supervisor autoreload' watches for code changes.

        This command provides a simulation of the Django dev server's
        auto-reloading mechanism that will restart all supervised processes.

        It's not quite as accurate as Django's autoreloader because it runs
        in a separate process, so it doesn't know the precise set of modules
        that have been loaded. Instead, it tries to watch all python files
        that are "nearby" the files loaded at startup by Django.
        """
        if args:
            raise CommandError("supervisor autoreload takes no arguments")
        live_dirs = self._find_live_code_dirs()
        reload_progs = self._get_autoreload_programs(cfg_file)

        def autoreloader():
            """
            Forks a subprocess to make the restart call.
            Otherwise supervisord might kill us and cancel the restart!
            """
            if os.fork() == 0:
                sys.exit(self.handle("restart", *reload_progs, **options))

        # Call the autoreloader callback whenever a .py file changes.
        # To prevent thrashing, limit callbacks to one per second.
        handler = CallbackModifiedHandler(callback=autoreloader,
                                          repeat_delay=1,
                                          patterns=AUTORELOAD_PATTERNS,
                                          ignore_patterns=AUTORELOAD_IGNORE,
                                          ignore_directories=True)

        # Try to add watches using the platform-specific observer.
        # If this fails, print a warning and fall back to the PollingObserver.
        # This will avoid errors with e.g. too many inotify watches.
        from watchdog.observers import Observer
        from watchdog.observers.polling import PollingObserver
        
        observer = None
        for ObserverCls in (Observer, PollingObserver):
            observer = ObserverCls()
            try:
                for live_dir in set(live_dirs):
                    observer.schedule(handler, live_dir, True)
                break
            except Exception:
                print>>sys.stderr, "COULD NOT WATCH FILESYSTEM USING"
                print>>sys.stderr, "OBSERVER CLASS: ", ObserverCls
                traceback.print_exc()
                observer.start()
                observer.stop()

        # Fail out if none of the observers worked.
        if observer is None:
            print>>sys.stderr, "COULD NOT WATCH FILESYSTEM"
            return 1

        # Poll if we have an observer.
        # TODO: Is this sleep necessary?  Or will it suffice
        # to block indefinitely on something and wait to be killed?
        observer.start()
        try:
            while True:
                time.sleep(1)
        except KeyboardInterrupt:
            observer.stop()
        observer.join()
        return 0

    def _get_autoreload_programs(self,cfg_file):
        """Get the set of programs to auto-reload when code changes.

        Such programs will have autoreload=true in their config section.
        This can be affected by config file sections or command-line
        arguments, so we need to read it out of the merged config.
        """
        cfg = RawConfigParser()
        cfg.readfp(cfg_file)
        reload_progs = []
        for section in cfg.sections():
            if section.startswith("program:"):
                try:
                    if cfg.getboolean(section,"autoreload"):
                        reload_progs.append(section.split(":",1)[1])
                except NoOptionError:
                    pass
        return reload_progs

    def _find_live_code_dirs(self):
        """Find all directories in which we might have live python code.

        This walks all of the currently-imported modules and adds their
        containing directory to the list of live dirs.  After normalization
        and de-duplication, we get a pretty good approximation of the
        directories on sys.path that are actively in use.
        """
        live_dirs = []
        for mod in sys.modules.values():
            #  Get the directory containing that module.
            #  This is deliberately casting a wide net.
            try:
                dirnm = os.path.dirname(mod.__file__)
            except AttributeError:
                continue
            #  Normalize it for comparison purposes.
            dirnm = os.path.realpath(os.path.abspath(dirnm))
            if not dirnm.endswith(os.sep):
                dirnm += os.sep
            #  Check that it's not an egg or some other wierdness
            if not os.path.isdir(dirnm):
                continue
            #  If it's a subdir of one we've already found, ignore it.
            for dirnm2 in live_dirs:
                if dirnm.startswith(dirnm2):
                    break
            else:
                #  Remove any ones we've found that are subdirs of it.
                live_dirs = [dirnm2 for dirnm2 in live_dirs\
                                    if not dirnm2.startswith(dirnm)]
                live_dirs.append(dirnm)
        return live_dirs


class OnDemandStringIO(object):
    """StringIO standin that demand-loads its contents and resets on EOF.

    This class is a little bit of a hack to make supervisord reloading work
    correctly.  It provides the readlines() method expected by supervisord's
    config reader, but it resets itself after indicating end-of-file.  If
    the supervisord process then SIGHUPs and tries to read the config again,
    it will be re-created and available for updates.
    """

    def __init__(self, callback, *args, **kwds):
        self._fp = None
        self.callback = callback
        self.args = args
        self.kwds = kwds

    @property
    def fp(self):
        if self._fp is None:
            self._fp = StringIO(self.callback(*self.args, **self.kwds))
        return self._fp

    def read(self, *args, **kwds):
        data = self.fp.read(*args, **kwds)
        if not data:
            self._fp = None
        return data

    def readline(self, *args, **kwds):
        line = self.fp.readline(*args, **kwds)
        if not line:
            self._fp = None
        return line
