# Copyright (c) 2012 OpenStack Foundation.
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

from __future__ import print_function

import errno
import gc
import logging
import os
import pprint
import socket
import sys
import traceback

import eventlet.backdoor
import greenlet

from oslo_service._i18n import _LI
from oslo_service import _options


LOG = logging.getLogger(__name__)


class EventletBackdoorConfigValueError(Exception):
    def __init__(self, port_range, help_msg, ex):
        msg = ('Invalid backdoor_port configuration %(range)s: %(ex)s. '
               '%(help)s' %
               {'range': port_range, 'ex': ex, 'help': help_msg})
        super(EventletBackdoorConfigValueError, self).__init__(msg)
        self.port_range = port_range


def _dont_use_this():
    print("Don't use this, just disconnect instead")


def _dump_frame(f, frame_chapter):
    co = f.f_code
    print(" %s Frame: %s" % (frame_chapter, co.co_name))
    print("     File: %s" % (co.co_filename))
    print("     Captured at line number: %s" % (f.f_lineno))
    co_locals = set(co.co_varnames)
    if len(co_locals):
        not_set = co_locals.copy()
        set_locals = {}
        for var_name in f.f_locals.keys():
            if var_name in co_locals:
                set_locals[var_name] = f.f_locals[var_name]
                not_set.discard(var_name)
        if set_locals:
            print("     %s set local variables:" % (len(set_locals)))
            for var_name in sorted(set_locals.keys()):
                print("       %s => %r" % (var_name, f.f_locals[var_name]))
        else:
            print("     0 set local variables.")
        if not_set:
            print("     %s not set local variables:" % (len(not_set)))
            for var_name in sorted(not_set):
                print("       %s" % (var_name))
        else:
            print("     0 not set local variables.")
    else:
        print("     0 Local variables.")


def _detailed_dump_frames(f, thread_index):
    i = 0
    while f is not None:
        _dump_frame(f, "%s.%s" % (thread_index, i + 1))
        f = f.f_back
        i += 1


def _find_objects(t):
    return [o for o in gc.get_objects() if isinstance(o, t)]


def _print_greenthreads(simple=True):
    for i, gt in enumerate(_find_objects(greenlet.greenlet)):
        print(i, gt)
        if simple:
            traceback.print_stack(gt.gr_frame)
        else:
            _detailed_dump_frames(gt.gr_frame, i)
        print()


def _print_nativethreads():
    for threadId, stack in sys._current_frames().items():
        print(threadId)
        traceback.print_stack(stack)
        print()


def _parse_port_range(port_range):
    if ':' not in port_range:
        start, end = port_range, port_range
    else:
        start, end = port_range.split(':', 1)
    try:
        start, end = int(start), int(end)
        if end < start:
            raise ValueError
        return start, end
    except ValueError as ex:
        raise EventletBackdoorConfigValueError(
            port_range, ex, _options.help_for_backdoor_port)


def _listen(host, start_port, end_port, listen_func):
    try_port = start_port
    while True:
        try:
            return listen_func((host, try_port))
        except socket.error as exc:
            if (exc.errno != errno.EADDRINUSE or
               try_port >= end_port):
                raise
            try_port += 1


def _try_open_unix_domain_socket(socket_path):
    try:
        return eventlet.listen(socket_path, socket.AF_UNIX)
    except socket.error as e:
        if e.errno != errno.EADDRINUSE:
            # NOTE(harlowja): Some other non-address in use error
            # occurred, since we aren't handling those, re-raise
            # and give up...
            raise
        else:
            # Attempt to remove the file before opening it again.
            try:
                os.unlink(socket_path)
            except OSError as e:
                if e.errno != errno.ENOENT:
                    # NOTE(harlowja): File existed, but we couldn't
                    # delete it, give up...
                    raise
            return eventlet.listen(socket_path, socket.AF_UNIX)


def _initialize_if_enabled(conf):
    conf.register_opts(_options.eventlet_backdoor_opts)
    backdoor_locals = {
        'exit': _dont_use_this,      # So we don't exit the entire process
        'quit': _dont_use_this,      # So we don't exit the entire process
        'fo': _find_objects,
        'pgt': _print_greenthreads,
        'pnt': _print_nativethreads,
    }

    if conf.backdoor_port is None and conf.backdoor_socket is None:
        return None

    if conf.backdoor_socket is None:
        start_port, end_port = _parse_port_range(str(conf.backdoor_port))
        sock = _listen('localhost', start_port, end_port, eventlet.listen)
        # In the case of backdoor port being zero, a port number is assigned by
        # listen().  In any case, pull the port number out here.
        where_running = sock.getsockname()[1]
    else:
        sock = _try_open_unix_domain_socket(conf.backdoor_socket)
        where_running = conf.backdoor_socket

    # NOTE(johannes): The standard sys.displayhook will print the value of
    # the last expression and set it to __builtin__._, which overwrites
    # the __builtin__._ that gettext sets. Let's switch to using pprint
    # since it won't interact poorly with gettext, and it's easier to
    # read the output too.
    def displayhook(val):
        if val is not None:
            pprint.pprint(val)
    sys.displayhook = displayhook

    LOG.info(
        _LI('Eventlet backdoor listening on %(where_running)s for'
            ' process %(pid)d'),
        {'where_running': where_running, 'pid': os.getpid()}
    )
    thread = eventlet.spawn(eventlet.backdoor.backdoor_server, sock,
                            locals=backdoor_locals)
    return (where_running, thread)


def initialize_if_enabled(conf):
    where_running_thread = _initialize_if_enabled(conf)
    if not where_running_thread:
        return None
    else:
        where_running, _thread = where_running_thread
        return where_running


def _main():
    import eventlet
    eventlet.monkey_patch(all=True)

    from oslo_config import cfg

    logging.basicConfig(level=logging.DEBUG)

    conf = cfg.ConfigOpts()
    conf.register_cli_opts(_options.eventlet_backdoor_opts)
    conf(sys.argv[1:])

    where_running_thread = _initialize_if_enabled(conf)
    if not where_running_thread:
        raise RuntimeError("Did not create backdoor at requested location")
    else:
        _where_running, thread = where_running_thread
        thread.wait()


if __name__ == '__main__':
    # simple CLI for testing
    _main()
