#!/skynet/python/bin/python -sBtt
from __future__ import print_function, absolute_import

import os
import sys
import grp
import time
import errno
import fcntl
import shlex
import struct
import random
import socket
import signal
import select
import argparse
import contextlib
import subprocess as subproc
from pprint import pprint as pp

import yaml

from api.skycore import Registry, ServiceManager, get_skycore_root, get_data_root, rpc_client, wrap_exception
from api.srvmngr import getRoot
from api.logger import constructLogger
from kernel.util.sys.user import getUserName

import api.skycore.errors as exceptions


# Exit codes:
# 0 - success
# 1 - error
# 2 - lockfile is busy
# 3 - autostart disabled
# >3 - command specific errors


def _get_skycore_revision(path=None):
    if path is not None:
        info_path = os.path.join(path, '.info')
    elif os.getenv('SKYNET_ROOT'):
        info_path = os.path.join(os.getenv('SKYNET_ROOT'), '.info')
    elif os.getenv('SKYNET_SUPERVISOR_PATH'):
        info_path = os.path.join(os.getenv('SKYNET_SUPERVISOR_PATH'), 'base', 'active')
    else:
        info_folder = os.path.dirname(os.path.realpath(__file__))
        next_folder = os.path.dirname(info_folder)
        info_path = None
        while info_folder != next_folder:
            path = os.path.join(info_folder, '.info')
            if os.path.exists(path) and os.path.isfile(path):
                info_path = path
                break

            info_folder = next_folder
            next_folder = os.path.dirname(info_folder)

    if info_path is None or not os.path.exists(info_path) or not os.path.isfile(info_path):
        raise RuntimeError("Cannot find skynet installation path!")

    data = yaml.load(open(info_path, 'rb'), Loader=yaml.SafeLoader)
    return data['skycore rev']


def _chown(path, uid, gid):
    os.chown(path.replace('/', '\\') if sys.platform == 'cygwin' else path, uid, gid)


def _has_root():
    uname = os.uname()[0].lower()
    if uname == 'darwin' or uname.startswith('cygwin'):
        return os.getuid() == 0
    else:
        return os.getresuid()[2] == 0


@wrap_exception()
def _is_started(expect=True):
    client = rpc_client()
    sleeps = (0, 1.0, 1.0)
    for i, sleep in enumerate(sleeps):
        try:
            call = wrap_exception()(client.call)('ping')
            wrap_exception()(call.wait)(10.)
            if not expect and (i + 1) < len(sleeps):
                time.sleep(sleep)
                continue
            return True
        except (exceptions.SkycoreError, socket.error):
            if not expect:
                return False
            if (i + 1) < len(sleeps):
                time.sleep(sleep)

    return False


class PyrasiteIPC(object):
    """Pyrasite Inter-Python Communication.

    This object is used in communicating to or from another Python process.

    It can perform a variety of tasks:

    - Injection of the :class:`pyrasite.ReversePythonConnection` payload via
      :meth:`PyrasiteIPC.connect()`, which causes the process to connect back
      to a port that we are listening on. The connection with the process is
      then available via `self.sock`.

    - Python code can then be executed in the process using
      :meth:`PyrasiteIPC.cmd`. Both stdout and stderr are returned.

    - Low-level communication with the process, both reliably (via a length
      header) or unreliably (raw data, ideal for use with netcat) with a
      :class:`pyrasite.ReversePythonConnection` payload, via
      :meth:`PyrasiteIPC.send(data)` and :meth:`PyrasiteIPC.recv(data)`.

    The :class:`PyrasiteIPC` is subclassed by
    :class:`pyrasite.tools.gui.Process` as well as
    :class:`pyrasite.reverse.ReverseConnection`.

    """
    # Allow subclasses to disable this and just send/receive raw data, as
    # opposed to prepending a length header, to ensure reliability. The reason
    # to enable 'unreliable' connections is so we can still use our reverse
    # shell payloads with netcat.
    reliable = True

    def __init__(self):
        super(PyrasiteIPC, self).__init__()
        self.sock = None
        self.server_sock = None
        self.hostname = None
        self.port = None
        self._title = None

    def __enter__(self):
        self.connect()
        return self

    def __exit__(self, *args, **kwargs):
        self.close()

    def connect(self):
        """
        Setup a communication socket with the process by injecting
        a reverse subshell and having it connect back to us.
        """
        self.listen()
        self.wait()

    def listen(self):
        """Listen on a random port"""
        for res in socket.getaddrinfo(
            self.hostname or 'localhost',
            self.port or None,
            socket.AF_UNSPEC,
            socket.SOCK_STREAM,
            0,
            0
        ):
            af, socktype, proto, canonname, sa = res
            try:
                self.server_sock = socket.socket(af, socktype, proto)
                self.server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
                try:
                    self.server_sock.bind(sa)
                    self.server_sock.listen(1)
                except socket.error:
                    self.server_sock.close()
                    self.server_sock = None
                    continue
            except socket.error:
                self.server_sock = None
                continue
            break

        if not self.server_sock:
            raise Exception('pyrasite was unable to setup a ' +
                            'local server socket')
        else:
            self.hostname, self.port = self.server_sock.getsockname()[0:2]

    def wait(self):
        """Wait for the injected payload to connect back to us"""
        (clientsocket, address) = self.server_sock.accept()
        self.sock = clientsocket
        self.sock.settimeout(5)
        self.address = address

    def cmd(self, cmd):
        """
        Send a python command to exec in the process and return the output
        """
        self.send(cmd + '\n')
        return self.recv()

    def send(self, data):
        """Send arbitrary data to the process via self.sock"""
        header = ''.encode('utf-8')
        data = data.encode('utf-8')
        if self.reliable:
            header = struct.pack('<L', len(data))
        self.sock.sendall(header + data)

    def recv(self):
        """Receive a command from a given socket"""
        if self.reliable:
            header_data = self.recv_bytes(4)
            if len(header_data) == 4:
                msg_len = struct.unpack('<L', header_data)[0]
                data = self.recv_bytes(msg_len).decode('utf-8')
                if len(data) == msg_len:
                    return data
        else:
            return self.sock.recv(4096).decode('utf-8')

    def recv_bytes(self, n):
        """Receive n bytes from a socket"""
        data = ''.encode('utf-8')
        while len(data) < n:
            chunk = self.sock.recv(n - len(data))
            if not chunk:
                break
            data += chunk
        return data

    def close(self):
        if self.sock:
            self.sock.close()
        if getattr(self, 'server_sock', None):
            self.server_sock.close()


class FLock(object):
    def __init__(self, fn, use_locking=True, timeout=0.0):
        self.fn = fn
        self.fp = None
        self.priv = _has_root()
        self.use_locking = use_locking
        self.timeout = timeout

    def _ensure_perms(self):
        if not os.path.exists(self.fn):
            return

        try:
            gid = grp.getgrnam('skywheel').gr_gid
        except KeyError:
            gid = 0
        _chown(self.fn, 0 if self.priv else os.getuid(), gid if self.priv else os.getgid())
        os.chmod(self.fn, 0o664)

    def _acquire(self):
        if os.path.exists(self.fn) and not os.path.isfile(self.fn):
            if os.path.isdir(self.fn):
                import shutil
                shutil.rmtree(self.fn)
            else:
                os.unlink(self.fn)

        try:
            if os.path.isfile(self.fn):
                self.fp = open(self.fn, 'rb+')
            else:
                self.fp = open(self.fn, 'wb+')
        except IOError:
            raise exceptions.AuthorizationError('Don`t have root privileges')

        start_time = time.time()
        while True:
            try:
                if self.use_locking:
                    fcntl.flock(self.fp, fcntl.LOCK_EX | fcntl.LOCK_NB)
                break
            except IOError:
                if time.time() - start_time > self.timeout:
                    print('lock at "%s" held by other process' % (self.fn,), file=sys.stderr)
                    raise SystemExit(2)
                else:
                    time.sleep(1)

        self._ensure_perms()  # FIXME possible race here, chown with fchown/fchmod
        return self.fp

    def _release(self):
        if self.fp:
            if self.use_locking:
                fcntl.flock(self.fp, fcntl.LOCK_UN)
            self.fp.close()
            self.fp = None

    def acquire(self):
        return self._acquire()

    def release(self):
        return self._release()

    def __enter__(self):
        return self.acquire()

    def __exit__(self, type, value, tb):
        self.release()


class Lockable(object):
    def __init__(self, flock=None):
        # rewind the flock
        if flock and flock.fp:
            flock.fp.seek(0)

        self._lock = flock

    @contextlib.contextmanager
    def with_lock(self, should_lock=True, flock=None, timeout=0.0):
        lock = flock or self._lock
        if lock and lock.fp:
            yield lock.fp
            return

        if not lock:
            lockfn = os.path.join(get_data_root(), 'skyctl.lock')
            lock = self._lock = FLock(lockfn, use_locking=should_lock, timeout=timeout)

        with lock as fp:
            yield fp


class FullHelpAction(argparse.Action):
    def __init__(self, option_strings, dest=argparse.SUPPRESS, default=argparse.SUPPRESS, help=argparse.SUPPRESS):
        super(FullHelpAction, self).__init__(
            option_strings=option_strings,
            dest=dest,
            default=default,
            nargs=0,
            help=help,
        )

    def __call__(self, parser, namespace, values, option_string=None):
        parser.print_help(full=True)
        parser.exit()


class SubParsersAction(argparse._SubParsersAction):
    class _ChoicesPseudoAction(argparse.Action):
        def __init__(self, name, help, hidden, parent):
            super(SubParsersAction._ChoicesPseudoAction, self).__init__(option_strings=[], dest=name, help=help)
            self._hidden = hidden
            self._parent = parent

        def get_parser(self):
            return self._parent._name_parser_map[self.dest]

    def add_parser(self, name, hidden=False, **kwargs):
        # set prog from the existing prefix
        if kwargs.get('prog') is None:
            kwargs['prog'] = '%s %s' % (self._prog_prefix, name)

        # create a pseudo-action to hold the choice help
        if 'help' in kwargs:
            help = kwargs.pop('help')
            if help != argparse.SUPPRESS:
                choice_action = self._ChoicesPseudoAction(name, help, hidden, self)
                self._choices_actions.append(choice_action)

        # create the parser and add it to the map
        parser = self._parser_class(**kwargs)
        parser._hidden = hidden
        self._name_parser_map[name] = parser
        return parser

    def _get_subactions(self, full=False):
        if full:
            return self._choices_actions
        else:
            return filter(lambda x: x._hidden is not True, self._choices_actions)


class SpecialFormatter(argparse.RawDescriptionHelpFormatter):
    def __init__(self, *args, **kwargs):
        self._show_hidden = kwargs.pop('full', False)
        super(SpecialFormatter, self).__init__(*args, **kwargs)

    def _iter_indented_subactions(self, action):
        get_subactions = getattr(action, '_get_subactions', None)
        if get_subactions is not None:
            self._indent()
            for subaction in get_subactions(full=self._show_hidden):
                yield subaction
            self._dedent()

    def _format_action_invocation(self, action):
        if isinstance(action, SubParsersAction._ChoicesPseudoAction):
            parser = action.get_parser()
            formatter = parser._get_formatter()
            formatter._prog = action.dest
            return formatter._format_usage(
                parser.usage,
                parser._actions,
                parser._mutually_exclusive_groups, ''
            ).strip()
        else:
            return super(SpecialFormatter, self)._format_action_invocation(action)


class ArgumentParser(argparse.ArgumentParser):
    def __init__(self, *args, **kwargs):
        kwargs['add_help'] = False
        kwargs['formatter_class'] = SpecialFormatter
        add_full_help = kwargs.pop('add_full_help', False)
        super(ArgumentParser, self).__init__(*args, **kwargs)

        self.register('action', 'fullhelp', FullHelpAction)
        self.register('action', 'parsers', SubParsersAction)
        group = self.add_mutually_exclusive_group()
        prefix_chars = kwargs.get('prefix_chars', '-')
        default_prefix = '-' if '-' in prefix_chars else prefix_chars[0]
        group.add_argument(default_prefix+'h', default_prefix*2+'help', action='help', help='Show help and exit')
        if add_full_help:
            group.add_argument(
                default_prefix * 2 + 'help-all',
                action='fullhelp',
                help='Show full help including hidden options, and exit'
            )

    def _get_formatter(self, full=False):
        return self.formatter_class(prog=self.prog, full=full)

    def format_help(self, full=False):
        formatter = self._get_formatter(full=full)

        # description
        formatter.add_text(self.description)

        # usage
        formatter.add_usage(self.usage, self._actions,
                            self._mutually_exclusive_groups)

        # positionals, optionals and user-defined groups
        for action_group in self._action_groups:
            formatter.start_section(action_group.title)
            formatter.add_text(action_group.description)
            formatter.add_arguments(action_group._group_actions)
            formatter.end_section()

        # epilog
        formatter.add_text(self.epilog)

        # determine help from format above
        return formatter.format_help()

    def print_help(self, file=None, full=False):
        if file is None:
            file = sys.stdout
        self._print_message(self.format_help(full=full), file)

    def error(self, message):
        self._print_message("%s: error: %s\n\n" % (self.prog, message), sys.stderr)
        self.print_help()
        sys.exit(1)


class Subcommand(object):
    def init_subcommand(self, client_class, mode, alias=None, description=None, hidden=False):
        self._client_class = client_class
        self._mode = mode
        self._description = description
        self._alias = alias
        self._hidden = hidden

    @property
    def mode(self):
        return self._mode

    @property
    def description(self):
        return self._description

    @property
    def hidden(self):
        return self._hidden

    def init_parser(self, subparsers):
        parser = subparsers.add_parser(self.mode, help=self.description, hidden=self.hidden)
        self._init_parser(parser)

    @classmethod
    def _init_parser(cls, parser):
        pass


class SkycoreInitCmd(Lockable, Subcommand):
    mode = "init"
    description = "start skycore daemon"
    hidden = False

    @classmethod
    def _init_parser(cls, parser):
        parser.add_argument('--var', help='data root')
        parser.add_argument(
            '--oneshot',
            action='store_true',
            default=False,
            help='just update config and services and exit'
        )
        parser.add_argument(
            '--start-services',
            dest='start_services',
            action='store_true',
            default=False,
            help='also start all services')
        parser.add_argument(
            '--restart-all-services',
            dest='restart_all_services',
            action='store_true',
            default=False,
            help='force all running services to restart')
        parser.add_argument(
            '--skydeps-changed',
            dest='skydeps_changed',
            action='store_true',
            default=False,
            help='force skydeps dependent services to restart')
        parser.add_argument(
            '--freeze-updates-i-know-what-i-am-doing',
            action='store_true',
            default=False,
            help=argparse.SUPPRESS)
        parser.add_argument(
            '--developer-mode',
            action='store_true',
            default=False,
            help=argparse.SUPPRESS)
        parser.add_argument(
            '--wait',
            type=float,
            default=0.0,
            help='wait specified time for lock if it is busy')
        parser.add_argument(
            '--wait-before-exit',
            type=float,
            default=0,
            metavar='SECONDS',
            help='after finish wait until total execution time become greater than SECONDS (for macosx launchd)',
        )

    def run(self, args, log):
        start_time = time.time()

        if args.oneshot and args.start_services:
            print("ERROR: options --oneshot and --start-services are mutually exclusive", file=sys.stderr)
            return 1

        try:
            with self.with_lock(should_lock=args.lock, timeout=args.wait) as fp:
                autoservices = True
                data = fp.read()
                if 'autoservices' in data:
                    autoservices = 'autoservices: enabled' in data
                if args.start_services:
                    autoservices = True
                if 'restart_all_services' in data:
                    args.restart_all_services = True

                fp.seek(0)
                fp.truncate()
                fp.write('autostart: enabled\n')
                fp.write('autoservices: %s\n' % ('enabled' if autoservices else 'disabled',))

                started = _is_started()
                if started and not args.start_services:
                    print('ERROR: skycore is running already', file=sys.stderr)
                    return 0

                if not started:
                    self._prepare_cgroups()
                    self._start(args)

                if args.start_services:
                    self._start_services()

        except exceptions.SkycoreError as ex:
            print('ERROR: %s' % ex, file=sys.stderr)
            return 1

        if args.wait_before_exit:
            exec_time = time.time() - start_time
            if exec_time < args.wait_before_exit:
                time.sleep(args.wait_before_exit - exec_time)

        return 0

    def _drop_tree(self, root):
        if not os.path.exists(root):
            return

        for sub in os.listdir(root):
            subpath = os.path.join(root, sub)
            if os.path.isdir(subpath):
                self._drop_tree(subpath)

        tasks = os.path.join(root, 'tasks')
        deadline = time.time() + 30

        while True:
            for sig in (signal.SIGSTOP, signal.SIGKILL, signal.SIGCONT):
                for pid in open(tasks, mode='rb').read().split():
                    if not pid:
                        continue
                    try:
                        os.kill(int(pid), sig)
                    except OSError:
                        pass

            pids = open(tasks, mode='rb').read()

            if time.time() > deadline or not pids:
                if not pids:
                    os.rmdir(root)
                break

            time.sleep(1)

    def _update_tree(self, root, config, level=0):
        for tryout in [0, 1] if level == 0 else [1]:
            try:
                if not os.path.exists(root):
                    try:
                        os.mkdir(root)
                    except OSError as ex:
                        if ex.errno == errno.EROFS:
                            print('Warning: failed to create "{}", Read-only file system'.format(root), file=sys.stderr)
                            break
                        else:
                            raise
                else:
                    # ignore read-only controllers
                    if os.statvfs(root).f_flag & 0x1:
                        break

                for key, value in sorted(config.items(), key=lambda kv: [isinstance(kv[1], dict), kv]):
                    fpath = os.path.join(root, key)

                    if isinstance(value, dict):
                        self._update_tree(os.path.join(root, key), value, level=level + 1)
                        continue

                    with open(fpath, 'wb') as fp:
                        fp.write(str(value))
            except BaseException:
                if tryout == 0:
                    self._drop_tree(root)
                else:
                    raise
            else:
                break

    def _prepare_cgroups(self):
        cgroup_path = '/sys/fs/cgroup'

        if not os.path.exists(cgroup_path) or not os.path.isdir(cgroup_path):
            return

        configuration = {
            'memory': {
                'skycore': {
                    'memory.use_hierarchy': 1,
                    'core': {
                        'memory.limit_in_bytes': '1G',
                    },
                    'services': {
                        'memory.limit_in_bytes': '1G',
                    },
                },
            },
            'cpu': {
                'skycore': {
                    'core': {},
                    'services': {},
                },
            },
            'freezer': {
                'skycore': {
                    'core': {},
                    'services': {},
                },
            },
            'blkio': {
                'skycore': {
                    'core': {},
                    'services': {},
                },
            },
            'devices': {
                'skycore': {
                    'core': {},
                    'services': {},
                },
            },
        }

        for controller, config in configuration.items():
            controller_path = os.path.join(cgroup_path, controller)
            if not os.path.exists(controller_path) or not os.path.isdir(controller_path):
                continue

            for root_cgroup, cfg in config.items():
                self._update_tree(os.path.join(controller_path, root_cgroup), cfg)

        # store base config in a file to synchronize with skycore
        import yaml
        with open(os.path.join(get_data_root(), 'skycore.cgroups'), 'wb') as f:
            configuration['__cgroup_path'] = cgroup_path
            yaml.dump(configuration, f, default_flow_style=False, Dumper=getattr(yaml, 'CSafeDumper', yaml.SafeDumper))
            f.flush()
            os.fsync(f.fileno())

    def _start(self, args):
        self_link = os.path.abspath(__file__)
        while True:
            skynet_path = os.path.dirname(os.path.dirname(self_link))  # we're in %skynet%/startup/skyctl
            if os.path.exists(os.path.join(skynet_path, 'startup', 'skyctl')):
                break

            skynet_path = None
            next_link = os.readlink(self_link)
            if not os.path.isabs(next_link):
                next_link = os.path.abspath(os.path.join(os.path.dirname(self_link), next_link))
            if next_link == self_link:
                break
            self_link = next_link

        if skynet_path is None:
            skynet_path = '/skynet'

        supervisor_path = getRoot()
        base_path = get_skycore_root()

        if _has_root():
            user = 'root'
        else:
            user = None

        # Export PYTHONPATH plus library path
        python_path = os.environ.get('PYTHONPATH', '').split(':')
        python_path.insert(0, os.path.join(base_path, 'lib'))
        path = os.environ.get('PATH', '').split(':')
        if sys.platform == 'darwin':
            path.append('/usr/local/bin')
            path.append('/opt/local/bin')

        environ = {
            'PYTHONPATH': ':'.join(python_path),
            'PATH': ':'.join(path),
        }

        # Export all SKYNET* environment variables
        for key, value in os.environ.items():
            if key.startswith('SKYNET'):
                environ[key] = value

        oneshot = getattr(args, 'oneshot', False)

        cmd = [
            os.path.join(base_path, 'bin', 'skycore'),
            '-d', args.var or get_data_root(),
            '-p', '10005',
            '--skynetdir', skynet_path,
            '--supervisordir', supervisor_path,
            '--revision', 'dev' if args.developer_mode else _get_skycore_revision(),
        ] + (['-u', user] if user else []) + (
            ['--oneshot'] if oneshot else []
        ) + (
            ['--restart-all-services'] if args.restart_all_services else []
        ) + (
            ['--skydeps-changed'] if args.skydeps_changed else []
        ) + (
            ['--freeze-updates-i-know-what-i-am-doing'] if args.freeze_updates_i_know_what_i_am_doing else []
        )

        proc = subproc.Popen(
            cmd,
            env=environ,
            close_fds=True,
            stdout=None if oneshot else subproc.PIPE,
            stderr=None if oneshot else subproc.PIPE,
            cwd='/',
            preexec_fn=self._preexec,
        )

        if oneshot:
            self._wait_oneshot(proc)
        else:
            self._wait_for_start(proc)

    def _preexec(self):
        os.setpgrp()
        self._move_to_cgroup()

    def _move_to_cgroup(self):
        cgroup_path = '/sys/fs/cgroup'
        cgroup = 'skycore/core'

        if not os.path.exists(cgroup_path) or not os.path.isdir(cgroup_path):
            return

        pid = os.getpid()

        for section in os.listdir(cgroup_path):
            section_path = os.path.join(cgroup_path, section)
            # ignore read-only controllers
            if os.statvfs(section_path).f_flag & 0x1:
                continue

            target_path = os.path.join(section_path, cgroup)
            if not os.path.exists(target_path) or not os.path.isdir(target_path):
                # If we have no target cgroup in this controller -- use top one
                # e.g. if we have no freezer/skycore/core => use freezer/
                target_path = section_path

            tasks_file = os.path.join(target_path, 'tasks')
            if not os.path.exists(tasks_file) or not os.path.isfile(tasks_file):
                continue
            open(tasks_file, 'wb').write('%d\n' % (pid, ))

    def _wait_oneshot(self, proc):
        out, err = proc.communicate()
        if proc.returncode and proc.returncode != -9:
            print('ERROR: skycore process died with exitcode %d' % (proc.returncode,), file=sys.stderr)
            raise SystemExit(proc.returncode)

    def _wait_for_start(self, proc):
        isrunning = False

        while proc.poll() is None and not isrunning:
            try:
                isrunning = _is_started()
            except Exception:
                isrunning = False
            if not isrunning:
                time.sleep(0.1)

        if not isrunning:
            assert proc.poll() is not None, "we should not be here"

            print('ERROR: skycore process died with exitcode %d' % (proc.poll(), ), file=sys.stderr)
            stdout, stderr = proc.communicate()
            if stderr:
                print(stderr, file=sys.stderr, end='')
            raise SystemExit(1)

        elif isrunning:
            assert proc.poll() is None, 'skycore died after reported it is running fine!'
            # self.server().closefds()  # FIXME ?
            for stream in proc.stdout, proc.stderr:
                stream.close()

            if not _is_started():
                print("ERROR: skycore process doesn't respond to pings", file=sys.stderr)
                raise SystemExit(1)

    def _start_services(self):
        client = ServiceManager()
        for attempt in xrange(3):
            try:
                nslist = client.list_namespaces()
                for ns in nslist:
                    client.start_services(ns)
            except Exception as e:
                print('[start_services] [%d/%d] ERROR: %s' % (attempt + 1, 3, e), file=sys.stderr)
            else:
                break


class SkycoreAutoinitCmd(Lockable, Subcommand):
    mode = "autoinit"
    description = "start skycore daemon with random delay (used in cron)"
    hidden = True

    @classmethod
    def _init_parser(cls, parser):
        SkycoreInitCmd._init_parser(parser)

    def run(self, args, log):
        sleep = random.randrange(0, 120)
        print('Sleeping %d secs' % (sleep, ))
        time.sleep(sleep)

        try:
            with self.with_lock(should_lock=args.lock) as fp:
                data = fp.read()
                if 'autostart: disabled' in data:
                    print('autostart disabled', file=sys.stderr)
                    return 3
                if 'autoservices' in data and 'autoservices: enabled' not in data and args.start_services:
                    print('WARNING: services autostart disabled', file=sys.stderr)
                    args.start_services = False
                if 'restart_all_services' in data:
                    args.restart_all_services = True

                return SkycoreInitCmd(flock=self._lock).run(args, log)
        except exceptions.SkycoreError as ex:
            print('ERROR: %s' % ex, file=sys.stderr)
            return 1


class SkycoreDebugSessionCmd(Lockable, Subcommand):
    mode = "run-debug-console"
    description = "run REPL console attached to skycore server (requires root privileges)"
    hidden = True

    @classmethod
    def _init_parser(cls, parser):
        parser.add_argument('-p', '--port', type=int, default=9091)

    def run(self, args, log):
        ipc = PyrasiteIPC()
        ipc.hostname = '::'
        ipc.port = args.port
        ipc.listen()
        print("bound to [%s]:%s" % (ipc.hostname, ipc.port))

        client = rpc_client()
        try:
            call = wrap_exception()(client.call)('run_debug_console', args.port)
            wrap_exception()(call.wait)(90)
        except exceptions.SkycoreError as ex:
            print('ERROR: %s' % (ex,), file=sys.stderr)
            return 1

        ipc.wait()
        prompt, payload = ipc.recv().split('\n', 1)

        print(payload)

        try:
            import readline  # noqa
        except ImportError:
            pass

        # py3k compat
        input_ = __builtins__.__dict__.get('raw_input', input)

        try:
            while True:
                try:
                    input_line = input_(prompt)
                except EOFError:
                    input_line = 'exit()'
                    print('')
                except KeyboardInterrupt:
                    input_line = 'None'
                    print('')

                ipc.send(input_line)
                payload = ipc.recv()
                if payload is None:
                    break
                prompt, payload = payload.split('\n', 1)
                if payload != '':
                    print(payload)
        except:
            print('')
            raise
        finally:
            ipc.close()

        return 0


class SkycoreForceUpdateCmd(Lockable, Subcommand):
    mode = 'force-update'
    description = 'trigger config and services update (requires root privileges)'
    hidden = True

    def run(self, args, log):
        client = rpc_client()
        try:
            call = wrap_exception()(client.call)('force_update')
            wrap_exception()(call.wait)(300)
        except exceptions.SkycoreError as ex:
            print('ERROR: %s' % (ex,), file=sys.stderr)
            return 1

        print("Done.")
        return 0


class SkycoreShutdownCmd(Lockable, Subcommand):
    mode = "shutdown"
    description = "shutdown skycore server"
    hidden = False

    @classmethod
    def _init_parser(cls, parser):
        parser.add_argument('-t', '--timeout', type=int, default=90, help='wait timeout')
        parser.add_argument(
            '--stop-services',
            dest='stop_services', action='store_true', default=False,
            help='also stop all services in all namespaces')
        parser.add_argument(
            '--keep-autostart',
            dest='keep_autostart', action='store_true',
            default=os.getenv('SKYCTL_KEEP_AUTOSTART', '') not in ('', '0'),
            help='do not disable autostart')
        parser.add_argument(
            '--wait',
            type=float,
            default=0.0,
            help='wait specified time for lock if it is busy')

    def run(self, args, log):
        self._check_data_root()

        try:
            with self.with_lock(should_lock=args.lock, timeout=args.wait) as fp:
                if not args.keep_autostart:
                    autoservices = True
                    data = fp.read()
                    if 'autoservices' in data:
                        autoservices = 'autoservices: enabled' in data
                    if args.stop_services:
                        autoservices = False

                    fp.seek(0)
                    fp.truncate()
                    fp.write('autostart: disabled\n')
                    fp.write('autoservices: %s\n' % ('enabled' if autoservices else 'disabled',))

                was_running = _is_started(True)

                log.info("shutting down skycore (current state = %r)", was_running)

                if was_running and args.stop_services:
                    log.info("stopping all services")
                    self._stop_services(args.timeout)

                presuccess = self._stop_by_rpc(args.timeout)
                log.info("shutdown with rpc ended with %r, will try other methods anyway", presuccess)
                # don't believe rpc, try other methods
                success = (
                    self._stop_by_pidfile(log, os.path.join(get_data_root(), 'skycore.pid'))
                    or self._stop_by_pidfile(log, '/run/skycore.pid')
                    or self._stop_by_port(log)
                )

                if was_running and not presuccess and not success:
                    log.error("failed to shutdown, skycore is still running.")
                    print('Failed to shutdown. Skycore is still running', file=sys.stderr)
                    return 1

        except Exception as ex:
            print('ERROR: %s' % (ex,), file=sys.stderr)
            return 1

        return 0

    def _check_data_root(self):
        try:
            get_data_root()
        except RuntimeError:
            print("Skycore data root is not found hence cannot be shutdown without extreme cruelty", file=sys.stderr)
            raise SystemExit(1)

    def _run_netstat(self, log):
        log.debug("will try to find pid in netstat")
        try:
            proc = subproc.Popen(['netstat', '-Wltnp4'], stdout=subproc.PIPE, stderr=subproc.PIPE)
        except Exception as e:
            log.error("failed to spawn netstat: %s", e)
            return

        stdout = []
        stderr = []
        start = time.time()
        outs = [proc.stdout, proc.stderr]
        timeout = 120
        while time.time() - start < timeout:
            try:
                rlist = select.select(outs, [], [], time.time() - start)[0]
            except select.error as e:
                if e.args[0] == errno.EINTR:
                    continue
                raise

            if proc.stdout in rlist:
                data = os.read(proc.stdout.fileno(), 1024)
                if data == '':
                    proc.stdout.close()
                    outs.remove(proc.stdout)
                stdout.append(data)

            if proc.stderr in rlist:
                data = os.read(proc.stderr.fileno(), 1024)
                if data == '':
                    proc.stderr.close()
                    outs.remove(proc.stderr)
                stderr.append(data)

        pid, sts = None, None
        while time.time() - start < timeout:
            try:
                pid, sts = os.waitpid(proc.pid, os.O_NONBLOCK)
            except EnvironmentError as e:
                if e.errno == errno.EAGAIN:
                    time.sleep(1)
                    continue
                elif e.errno != errno.ECHILD:
                    log.error("netstat(%d) failed with %s", proc.pid, e)
                    if stderr:
                        log.error("stderr is %s", ''.join(stderr))
                    return
            if pid == proc.pid:
                break

        if pid != proc.pid:
            log.error("netstat(%d) execution timed out, killing", proc.pid)
            proc.kill()
            return

        if os.WIFEXITED(sts):
            sts = os.WEXITSTATUS(sts)
            if sts != 0:
                log.error("netstat(%d) exited with non-zero exitcode %s", proc.pid, sts)
                if stderr:
                    log.error("stderr is %s", ''.join(stderr))
                return
            return ''.join(stdout)

        if os.WIFSIGNALED(sts):
            log.error("netstat(%d) killed with %s", proc.pid, os.WTERMSIG(sts))
            if stderr:
                log.error("stderr is %s", ''.join(stderr))
            return

        log.error("netstat(%d) killed with unknown status %s", proc.pid, sts)
        if stderr:
            log.error("stderr is %s", ''.join(stderr))

    def _stop_services(self, timeout):
        client = ServiceManager()
        for attempt in xrange(3):
            try:
                nslist = client.list_namespaces()
                for ns in nslist:
                    client.stop_services(ns, timeout=timeout)
            except Exception as e:
                print('[stop_services] [%d/%d] ERROR: %s' % (attempt + 1, 3, e), file=sys.stderr)
            else:
                break

    def _stop_by_rpc(self, timeout):
        try:
            if not _is_started(False):
                return False

            client = rpc_client()
            call = wrap_exception()(client.call)('shutdown')
            wrap_exception()(call.wait)(timeout)

            # wait until skycore really stopped
            for _ in xrange(10):
                if not _is_started(False):
                    return True
                time.sleep(1)
        except exceptions.SkycoreError as ex:
            print('ERROR: %s' % (ex,), file=sys.stderr)

        return False

    def _stop_by_pidfile(self, log, pidfile):
        if os.name != 'posix':
            log.info("not using pidile, we're not posix")
            return False

        try:
            if not os.path.exists(pidfile) or not os.path.isfile(pidfile):
                log.info("pidfile %r doesn't exist", pidfile)
                return False

            try:
                pid = int(open(pidfile).read().strip())
            except (EnvironmentError, ValueError) as e:
                log.error("failed to read pid from pidfile %r: %s", pidfile, e)
                raise Exception("failed to read pid from pidfile: %s" % (e,))

            def _check_pidcmd():
                cmdline = '/proc/%d/cmdline' % (pid,)
                if not os.path.exists(cmdline) or not os.path.isfile(cmdline):
                    log.info("pid %d from pidfile %r doesn't exist", pid, pidfile)
                    return False

                try:
                    pidcmd = open(cmdline).read().replace('\x00', '').strip()
                    if pidcmd != 'skycore':
                        log.info("pid %d from pidfile %r isn't skycore: %r", pid, pidfile, pidcmd)
                        return False
                except EnvironmentError as e:
                    log.error("failed to get cmdline of pid %d from pidfile %r: %s", pid, pidfile, e)
                    raise Exception("failed to get cmdline of pid %d: %s" % (pid, e))

                return True

            pid_killed = False
            while _check_pidcmd():
                try:
                    os.kill(pid, 9)
                    pid_killed = True
                except EnvironmentError as e:
                    if e.errno == errno.ESRCH:
                        log.info("pid %d successfully killed", pid)
                        return True

                time.sleep(0.5)

            return pid_killed

        except Exception as ex:
            print('ERROR: %s' % (ex,), file=sys.stderr)

        return False

    def _stop_by_port(self, log):
        if os.name != 'posix':
            log.info("not using port, we're not posix")
            return False

        def _find_port_pid():
            try:
                s = socket.socket()
                s.bind(('127.0.0.1', 10005))
                s.listen(1)
            except EnvironmentError as e:
                if e.errno != errno.EADDRINUSE:
                    raise
            else:
                log.debug("port is free, won't kill by port")
                s.close()
                return

            output = self._run_netstat(log)
            if output is None:
                return

            pid = None

            for line in output.split('\n'):
                data = line.split()
                if len(data) < 7 or data[3] != '127.0.0.1:10005':
                    continue

                pid = data[6].split('/')[0]

                try:
                    pid = int(pid)
                except ValueError:
                    log.error("failed to parse pid from line %r", data[6])
                    raise Exception("failed to parse pid from line %r", data[6])

            if pid is None:
                log.info("skycore not found in netstat")
            else:
                cmdline = '/proc/%d/cmdline' % (pid,)
                if not os.path.exists(cmdline) or not os.path.isfile(cmdline):
                    log.info("pid %d from netstat doesn't exist", pid)
                    return

                try:
                    pidcmd = open(cmdline).read().replace('\x00', '').strip()
                    if pidcmd != 'skycore':
                        log.info("pid %d from netstat isn't skycore: %r", pid, pidcmd)
                        return
                except EnvironmentError as e:
                    log.error("failed to get cmdline of pid %d from netstat: %s", pid, e)
                    raise Exception("failed to get cmdline of pid %d: %s" % (pid, e))

            return pid

        try:
            pid = _find_port_pid()
            pid_killed = False

            while pid is not None:
                try:
                    os.kill(pid, 9)
                    pid_killed = True
                except EnvironmentError as e:
                    if e.errno == errno.ESRCH:
                        # process died before we could kill it, it's ok
                        log.info("pid %d successfully killed", pid)
                        return True

                time.sleep(0.5)
                pid = _find_port_pid()

            return pid_killed
        except Exception as ex:
            print('ERROR: %s' % (ex,), file=sys.stderr)

        return False


class BaseCmd(Lockable, Subcommand):
    def __init__(self, client_class, mode, alias=None, description=None, hidden=False, **kwargs):
        super(BaseCmd, self).__init__(**kwargs)
        self.init_subcommand(client_class, mode, alias, description, hidden)
        self._client = None

    @classmethod
    def _init_parser(cls, parser):
        parser.add_argument(
            '-f',
            '--format',
            choices=['human', 'repr', 'json'],
            default='human',
            help='output format')

    def run(self, args, log):
        self._client = self._client_class()
        func = getattr(self._client, self._alias or self._mode, None)
        if not func:
            print('ERROR: Unsupported call:', args.mode, file=sys.stderr)
            return 1

        try:
            res, retcode = self._run(func, args)
            if args.format == 'repr':
                pp(res)
            elif args.format == 'human':
                self._human_print(res)
            else:
                try:
                    import simplejson as json
                except ImportError:
                    json = None

                if json is None:
                    import json
                print(json.dumps(res))

        except exceptions.SkycoreError as ex:
            print('ERROR: %s' % ex, file=sys.stderr)
            return 1

        return retcode

    def _run(self, func, args):
        pass

    def _human_print(self, res):
        if isinstance(res, bool):
            print('Success' if res else 'Failed')
        elif isinstance(res, dict):
            d = [(k, 0, v) for k, v in res.iteritems()]

            while d:
                next_item = d.pop(0)
                key, indent, value = next_item

                if isinstance(value, dict):
                    d.insert(0, (key, indent, ''))
                    for insert_idx, (key2, value2) in enumerate(value.iteritems()):
                        d.insert(insert_idx + 1, (key2, indent + 1, value2))

                else:
                    indent_txt = '  ' * indent
                    try:
                        value_txt = str(value)
                    except:
                        value_txt = repr(value)

                    print('%s%s: %s' % (indent_txt, key, value_txt))
        elif isinstance(res, list) or isinstance(res, set):
            if not res:
                print("Empty list")
            else:
                for r in sorted(res):
                    print(r)
        else:
            print(res)


class RegistryCmd(BaseCmd):
    def __init__(self, *args, **kwargs):
        super(RegistryCmd, self).__init__(Registry, *args, **kwargs)

    def _run(self, func, args):
        return func(), 0


class QueryRegistryCmd(RegistryCmd):
    @classmethod
    def _init_parser(cls, parser):
        super(QueryRegistryCmd, cls)._init_parser(parser)
        parser.add_argument('path', nargs='*', default=None, help='services to work on (default: all)')

    def _run(self, func, args):
        return func(args.path), 0

    def _human_print(self, res):
        import yaml
        print(yaml.dump(dict(res), default_flow_style=0))


class ServiceManagerCmd(BaseCmd):
    def __init__(self, *args, **kwargs):
        super(ServiceManagerCmd, self).__init__(ServiceManager, *args, **kwargs)

    def _run(self, func, args):
        return func(), 0


class ListServiceManagerCmd(ServiceManagerCmd):
    @classmethod
    def _init_parser(cls, parser):
        super(ListServiceManagerCmd, cls)._init_parser(parser)
        parser.add_argument('namespace', help='namespace')

    def _run(self, func, args):
        return func(args.namespace), 0


class InstallServiceManagerCmd(ServiceManagerCmd):
    @classmethod
    def _init_parser(cls, parser):
        super(InstallServiceManagerCmd, cls)._init_parser(parser)
        parser.add_argument('namespace', help='namespace')
        parser.add_argument('path', help='path')

    def _run(self, func, args):
        return func(args.namespace, args.path), 0


class NamespaceServiceManagerCmd(ServiceManagerCmd):
    @classmethod
    def _init_parser(cls, parser):
        super(NamespaceServiceManagerCmd, cls)._init_parser(parser)
        parser.add_argument('namespace', nargs='?', default=None, help='namespace')
        parser.add_argument('services', nargs='*', default=None, help='services to work on (default: all)')

    def _run(self, func, args):
        res = {}
        if args.namespace:
            res[args.namespace] = func(args.namespace, args.services)
        else:
            # iterate over namespaces
            for ns in self._client.list_namespaces():
                res[ns] = func(ns, args.services)
        return res, 0

    def _human_print(self, res):
        if not res:
            print("Empty")
            return

        for ns, r in sorted(res.items()):
            print('{:20s}: {}'.format(ns, 'Success' if r else 'Failed'))


class CheckServiceManagerCmd(NamespaceServiceManagerCmd):
    @classmethod
    def _init_parser(cls, parser):
        super(CheckServiceManagerCmd, cls)._init_parser(parser)
        parser.add_argument(
            '--no-uptime',
            dest='no_uptime', action='store_true',
            default=False,
            help='do not show service uptime')

    def _run(self, func, args):
        self.no_uptime = args.no_uptime
        res = {}
        if args.namespace:
            res[args.namespace] = func(args.namespace, args.services, new_format=True)
            retcode = any(
                state['state'] != 'RUNNING' and
                state['state'] != 'PRERUNNING' for state in res[args.namespace].itervalues()
            )
        else:
            # iterate over namespaces
            retcode = 0
            for ns in self._client.list_namespaces():
                res[ns] = func(ns, args.services, new_format=True)
                retcode = retcode or any(
                    state['state'] != 'RUNNING' and state['state'] != 'PRERUNNING' for state in res[ns].itervalues()
                )
        return res, 4 if retcode else 0

    def _human_print(self, res):
        if not res:
            print("Empty")
        else:
            for ns, r in sorted(res.items()):
                print(ns+':')
                if not r:
                    print("Empty")
                else:
                    for service, state in sorted(r.items()):
                        print('{:20s}: {:20s} {:>13s} {}'.format(
                            service,
                            state['state'],
                            '' if self.no_uptime else state.get('state_uptime', ''),
                            state['version']))
                print('')


class StartServiceManagerCmd(NamespaceServiceManagerCmd):
    @classmethod
    def _init_parser(cls, parser):
        super(StartServiceManagerCmd, cls)._init_parser(parser)
        parser.add_argument(
            '--keep-autostart',
            dest='keep_autostart', action='store_true',
            default=os.getenv('SKYCTL_KEEP_AUTOSTART', '') not in ('', '0'),
            help='do not enable autostart')
        parser.add_argument(
            '--wait',
            type=float,
            default=0.0,
            help='wait specified time for lock if it is busy')

    def _run(self, func, args):
        with self.with_lock(should_lock=args.lock, timeout=args.wait) as fp:
            autostart = False
            autoservices = True
            data = fp.read()
            if 'autostart' in data:
                autostart = 'autostart: enabled' in data
            if 'autoservices' in data:
                autoservices = 'autoservices: enabled' in data
            if not args.keep_autostart:
                autoservices = True

            fp.seek(0)
            fp.truncate()
            fp.write('autostart: %s\n' % ('enabled' if autostart else 'disabled',))
            fp.write('autoservices: %s\n' % ('enabled' if autoservices else 'disabled',))

            return super(StartServiceManagerCmd, self)._run(func, args)


class StopServiceManagerCmd(NamespaceServiceManagerCmd):
    @classmethod
    def _init_parser(cls, parser):
        super(StopServiceManagerCmd, cls)._init_parser(parser)
        parser.add_argument(
            '--keep-autostart',
            dest='keep_autostart', action='store_true',
            default=os.getenv('SKYCTL_KEEP_AUTOSTART', '') not in ('', '0'),
            help='do not disable autostart')
        parser.add_argument(
            '--wait',
            type=float,
            default=0.0,
            help='wait specified time for lock if it is busy')

    def _run(self, func, args):
        with self.with_lock(should_lock=args.lock, timeout=args.wait) as fp:
            autostart = False
            autoservices = True
            data = fp.read()
            if 'autostart' in data:
                autostart = 'autostart: enabled' in data
            if 'autoservices' in data:
                autoservices = 'autoservices: enabled' in data
            if not args.keep_autostart:
                autoservices = False

            fp.seek(0)
            fp.truncate()
            fp.write('autostart: %s\n' % ('enabled' if autostart else 'disabled',))
            fp.write('autoservices: %s\n' % ('enabled' if autoservices else 'disabled',))

            return super(StopServiceManagerCmd, self)._run(func, args)


class ScriptServiceManagerCmd(ServiceManagerCmd):
    @classmethod
    def _init_parser(cls, parser):
        super(ScriptServiceManagerCmd, cls)._init_parser(parser)
        parser.add_argument('namespace', help='namespace')
        parser.add_argument('service', help='service name')


class GetFieldServiceManagerCmd(ScriptServiceManagerCmd):
    @classmethod
    def _init_parser(cls, parser):
        super(GetFieldServiceManagerCmd, cls)._init_parser(parser)
        parser.add_argument(
            '--raw',
            action='store_true',
            default=False,
            help='do not interpret incorporated variables'
        )
        parser.add_argument('field', help='descriptor field name')

    def _run(self, func, args):
        return func(args.namespace, args.service, args.field, args.raw), 0


class RunScriptServiceManagerCmd(ScriptServiceManagerCmd):
    @classmethod
    def _init_parser(cls, parser):
        super(RunScriptServiceManagerCmd, cls)._init_parser(parser)
        parser.add_argument('-q', '--quiet', action='count', default=0,
                            help='do not display mandatory warning about performed action')
        parser.add_argument('--no-act', dest='no_act', action='store_true', default=False,
                            help='do not exec script, only display it')
        parser.add_argument('script', help='script name')
        parser.add_argument('args', nargs=argparse.REMAINDER, type=str, default=[])

    def _run(self, func, args):
        res = func(args.namespace, args.service, 'scripts')
        if args.script not in res:
            print("ERROR: no such script %r in %r/%r" % (args.script, args.namespace, args.service), file=sys.stderr)
            return None, 1

        script = res[args.script]
        script = shlex.split(script)
        script += args.args
        if not args.quiet:
            print(
                "WARNING: running service scripts from non-skycore environment can lead to unexpected service state."
                "Use it on your own risk.", file=sys.stderr
            )
        if args.quiet < 2 and not args.no_act:
            print("Will run: %r" % (subproc.list2cmdline(script),), file=sys.stderr)

        if args.no_act:
            return script, 0

        env = func(args.namespace, args.service, 'env')
        e = os.environ.copy()
        if env is not None:
            e.update(env)

        os.execve(script[0], script, e)
        return None, 0


class ListScriptsServiceManagerCmd(ScriptServiceManagerCmd):
    def _run(self, func, args):
        return func(args.namespace, args.service, 'scripts'), 0

    def _human_print(self, res):
        if not res:
            print("No scripts defined")
        else:
            for name, script in sorted(res.items()):
                print("%s:\t%s" % (name, script))


class CmdDispatcher(object):
    SELFTEST_CMD = 'self-test'

    def __init__(self, command_logger):
        super(CmdDispatcher, self).__init__()
        self.parser = ArgumentParser(
            description='Single entrypoint of control for Skycore',
            epilog='Use `%(prog)s COMMAND -h` to get help for the COMMAND.\n'
                   'Some debug options are hidden, use `%(prog)s --help-all` to get help about all options.',
            add_full_help=True,
        )
        group = self.parser.add_mutually_exclusive_group()
        group.add_argument('--lock',
                           dest='lock',
                           default=os.getenv('SKYCTL_USE_LOCK', 1) not in ('', '0', 'no'),
                           action='store_true',
                           help='run with lock')
        group.add_argument('--no-lock', dest='lock', action='store_false', help='run without lock')
        self.subparsers = self.parser.add_subparsers(title='Subcommands', dest='mode', metavar='COMMAND')
        self.subparsers.add_parser(self.SELFTEST_CMD, help='Simple self-test. DO NOT RUN WITH SUDO!!!', hidden=True)
        self.cmds = {}
        self.command_logger = command_logger

    def register(self, cmds):
        if isinstance(cmds, list):
            for cmd in cmds:
                self._register_cmd(cmd)
        else:
            self._register_cmd(cmds)

    def run(self):
        args, argv = self.parser.parse_known_args()
        if argv:
            self.parser.print_help()
            return 1
        mode = args.mode
        if mode == self.SELFTEST_CMD:
            return self._run_tests(args)

        cmd = self.cmds[mode]
        return cmd.run(args, self.command_logger.log)

    def _register_cmd(self, cmd):
        cmd.init_parser(self.subparsers)
        self.cmds[cmd.mode] = cmd

    def _run_tests(self, args):
        # do some testing here
        test_results = {}
        failed = False
        for name, cmd in self.cmds.iteritems():
            try:
                sys.argv[1] = name
                self.run()
                test_results[name] = 'OK'
            except SystemExit:
                test_results[name] = 'OK (SystemExit)'
            except BaseException as ex:
                failed = True
                test_results[name] = 'FAILED with: {}'.format(repr(ex))

        print('='*40)
        for name, res in test_results.iteritems():
            print('{} {}'.format(name, res))

        return int(failed)


class CommandLogger(object):
    def __init__(self, file_name):
        try:
            self.log = constructLogger('', app='skyctl', filename=file_name).getChild(
                '{}:{}'.format(sys.argv[1] if len(sys.argv) > 1 else 'help', os.getpid())
            )
        except Exception:
            from logging import getLogger, DEBUG
            from logging.handlers import SysLogHandler
            root_log = getLogger('')
            self.log = root_log.getChild('skyctl.{}'.format(sys.argv[1] if len(sys.argv) > 1 else 'help'))
            root_log.setLevel(DEBUG)
            self.log.setLevel(DEBUG)
            handler = SysLogHandler('/dev/log')
            handler.setLevel(DEBUG)
            root_log.addHandler(handler)

        self.user = getUserName()
        self.start_time = time.time()

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        if self.log:
            self.log.info('{} {} took {}'.format(
                    self.user,
                    repr(sys.argv),
                    time.time() - self.start_time
                )
            )


def main():
    with CommandLogger('skyctl.log') as cmd_logger:
        disp = CmdDispatcher(cmd_logger)
        disp.register([
            SkycoreInitCmd(),
            SkycoreAutoinitCmd(),
            SkycoreShutdownCmd(),
            SkycoreDebugSessionCmd(),
            SkycoreForceUpdateCmd(),
            RegistryCmd(
                'pause',
                'pause_update',
                'Pause config updates. No new configs will be fetched from genisys, and services will not be updated too',
                hidden=True
            ),
            RegistryCmd(
                'unpause',
                'unpause_update',
                'Resume config updates',
                hidden=True
            ),
            RegistryCmd(
                'state',
                'state',
                'Get current config updater state',
                hidden=True
            ),
            QueryRegistryCmd(
                'query',
                'query_section',
                'Query config registry for some section',
                hidden=True
            ),
            ServiceManagerCmd(
                'stats',
                'get_stats',
                'Stop some or all services in the choosen namespace'
            ),
            ServiceManagerCmd(
                'list-ns',
                'list_namespaces',
                'List all available namespaces'
            ),
            ListServiceManagerCmd(
                'list',
                'list_services',
                'List services in the choosen namespace'
            ),
            CheckServiceManagerCmd(
                'check',
                'check_services',
                'Check some or all services current state in the choosen or all namespace'
            ),
            InstallServiceManagerCmd(
                'install',
                'install_tgz',
                'Install service(s) from tgz file. Note: service can be removed automatically,'
                ' if the namespace is controlled by genisys'
            ),
            StartServiceManagerCmd(
                'start',
                'start_services',
                'Start some or all services in the choosen or all namespace'
            ),
            NamespaceServiceManagerCmd(
                'restart',
                'restart_services',
                'Restart some or all services in the choosen or all namespace'
            ),
            StopServiceManagerCmd(
                'stop',
                'stop_services',
                'Stop some or all services in the choosen or all namespace'
            ),
            NamespaceServiceManagerCmd(
                'uninstall',
                'uninstall_services',
                'Uninstall some service(s) in the choosen namespace'
            ),
            GetFieldServiceManagerCmd(
                'get',
                'get_service_field',
                'Get service arbitrary field from scsd description'
            ),
            RunScriptServiceManagerCmd(
                'run-script',
                'get_service_field',
                'Run special service script from scsd description'
            ),
            ListScriptsServiceManagerCmd(
                'list-scripts',
                'get_service_field',
                'List available scripts for service'
            ),
        ])

        return disp.run()


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