#!/usr/bin/env python
# vim: fileencoding=utf8:et:sta:ai:sw=4:ts=4:sts=4

import argparse
import glob
import multiprocessing
import os
import signal
import sys
import tempfile

from feather import wsgi, monitor


DEFAULT_CLUSTER = monitor.Monitor.DEFAULT_CLUSTER


def control_dir(cluster_name):
    if cluster_name is None:
        cluster_name = DEFAULT_CLUSTER
    dirs = glob.glob(os.path.join(tempfile.gettempdir(),
            'feather-%s-*' % cluster_name))
    if dirs:
        return max(dirs)
    return os.path.join(tempfile.gettempdir(),
            'feather-%s-0' % cluster_name)

def master_pid(cluster_name):
    cd = control_dir(cluster_name)
    if cd is None:
        return None
    if not os.path.isdir(cd):
        return None
    return int(open(os.path.join(cd, 'master.pid')).read())

def print_cluster_status(cluster_dir, master_pid, worker_pids, to=sys.stdout):
    count = cluster_dir.rsplit('-', 1)[1]
    cluster = cluster_dir.rsplit(os.path.sep, 1)[1][8:-(len(count)+1)]
    if not (master_pid or worker_pids):
        to.write("Cluster '%s' %s:\n" % (cluster, count))

    with open(os.path.join(cluster_dir, 'master.pid'), 'r') as fp:
        mpid = fp.read().strip()
    if master_pid:
        to.write(str(mpid) + '\n')
    else:
        to.write('  master  : %s\n' % mpid)

    if master_pid and not worker_pids:
        return

    wpidfiles = glob.glob(os.path.join(cluster_dir, 'worker*.pid'))
    wpidfiles.sort()
    for wpidfile in wpidfiles:
        wid = wpidfile.rsplit(os.path.sep, 1)[1][6:-4]
        with open(wpidfile, 'r') as fp:
            wpid = fp.read().strip()
        if worker_pids:
            to.write(wpid + '\n')
        else:
            to.write('  worker %s: %s\n' % (wid, wpid))

NOMOD = object()
NOOBJ = object()

def get_imported_object(spec, delim=':'):
    mod, attr = spec.split(delim, 1)
    module = __import__(mod)
    for attribute in mod.split('.')[1:]:
        module = getattr(module, attribute, None)
        if module is None:
            sys.stderr.write("no module: %s\n" % mod)
            return NOMOD
    obj = getattr(module, attr, NOOBJ)
    if obj is NOOBJ:
        sys.stderr.write("no attribute: %s\n" % attr)
    return obj


def status_cmd(environ, args):
    if args.cluster:
        cdirs = glob.glob(os.path.join(tempfile.gettempdir(),
                'feather-%s-*' % args.cluster))
        if not cdirs:
            sys.stderr.write("no cluster '%s' active\n" % args.cluster)
            return 1
        print_cluster_status(cdirs[0], args.master, args.workers)
        for cdir in cdirs[1:]:
            sys.stdout.write('\n')
            print_cluster_status(cdir, args.master, args.workers)
        return 0

    if args.master or args.workers:
        cluster_dirs = glob.glob(os.path.join(tempfile.gettempdir(),
                'feather-%s-*' % (args.cluster or DEFAULT_CLUSTER)))
        cluster_dirs.sort(reverse=True)
        cluster_dirs = [cluster_dirs[0]]
    else:
        cluster_dirs = glob.glob(os.path.join(tempfile.gettempdir(),
                'feather-*'))
    if not cluster_dirs:
        sys.stdout.write("no active clusters\n")
        return 0

    print_cluster_status(cluster_dirs[0], args.master, args.workers)
    for cluster_dir in cluster_dirs[1:]:
        sys.stdout.write('\n')
        print_cluster_status(cluster_dir, args.master, args.workers)

    return 0

def start_cmd(environ, args):
    app = get_imported_object(args.wsgiapp)
    if app in (NOMOD, NOOBJ):
        return 1

    cd = control_dir(args.cluster)

    server = wsgi.server(
            (args.host, args.port),
            app,
            traceback_body=args.traceback_body,
            keepalive_timeout=args.keepalive_timeout)

    Mon = get_imported_object(args.monitor_class)
    if Mon in (NOMOD, NOOBJ):
        return 1

    mon = Mon(server,
            args.num_workers,
            user=args.user,
            group=args.group,
            control_dir=cd.rsplit('-', 1)[0],
            daemonize=not args.foreground)

    mon.serve()

    return 0

def stop_cmd(environ, args):
    sig = signal.SIGQUIT if args.graceful else signal.SIGTERM

    mp = master_pid(args.cluster)
    if mp is None:
        sys.stderr.write('no control dir for %s\n' %
                (args.cluster or DEFAULT_CLUSTER))
        return 1
    os.kill(mp, sig)
    return 0

def reload_cmd(environ, args):
    mp = master_pid(args.cluster)
    if mp is None:
        sys.stderr.write('no control dir for %s\n' %
                (args.cluster or DEFAULT_CLUSTER))
        return 1
    os.kill(mp, signal.SIGHUP)
    return 0


def main(environ, argv):
    parser = argparse.ArgumentParser(prog='featherctl')
    parser.add_argument('-c', '--cluster', action='store',
            help='feather cluster on which operate (or create)')
    subparsers = parser.add_subparsers()

    status_parser = subparsers.add_parser('status',
            help='output status and pids of all active clusters')
    status_parser.add_argument('-m', '--master', action='store_true',
            help='only show the master pid of a single cluster')
    status_parser.add_argument('-w', '--workers', action='store_true',
            help='only show the worker pids of a single cluster')
    status_parser.set_defaults(func=status_cmd)

    start_parser = subparsers.add_parser('start', help='start a new cluster')
    start_parser.add_argument('-H', '--host', default='0.0.0.0',
            help='server host/ip')
    start_parser.add_argument('-P', '--port', type=int, default=8000,
            help='server port')
    start_parser.add_argument('-u', '--user', type=str, default=None,
            help='system user under which workers should run')
    start_parser.add_argument('-g', '--group', type=str, default=None,
            help='system group under which workers should run')
    start_parser.add_argument('-t', '--traceback-body', action='store_true',
            help='include stack traces in 500 response bodies')
    start_parser.add_argument('-k', '--keepalive-timeout',
            type=int, default=30,
            help='seconds to hold open inactive HTTP connections. ' +
                    'set to 0 to turn off HTTP keepalive entirely')
    start_parser.add_argument('-n', '--num-workers',
            type=int, default=multiprocessing.cpu_count(),
            help='number of server worker processes to run')
    start_parser.add_argument('-f', '--foreground', action='store_true',
            default='false', help='run the feather master in the foreground')
    start_parser.add_argument('-m', '--monitor-class',
            default='feather.monitor:Monitor',
            help='Monitor class to use to run the cluster (specified the ' +
                    'same way as "wsgiapp")')
    start_parser.add_argument('wsgiapp',
            help='how to get the WSGI app, specified as "<import-path>:' +
                    '<app-attribute>". so if module bar in package foo ' +
                    'contains a WSGI app as variable "app", this would be ' +
                    '"foo.bar:app"')
    start_parser.set_defaults(func=start_cmd)

    reload_parser = subparsers.add_parser('reload',
            help="restart the cluster's workers")
    reload_parser.set_defaults(func=reload_cmd)

    stop_parser = subparsers.add_parser('stop', help='stop the cluster')
    stop_parser.add_argument('-f', '--hard', dest='graceful',
            action='store_false', default=True,
            help="hard-stop: don't let workers flush in-progress requests")
    stop_parser.set_defaults(func=stop_cmd)

    args = parser.parse_args()
    return args.func(environ, args) or 0


if __name__ == '__main__':
    exit(main(os.environ, sys.argv))
