#!/usr/bin/env python
# -- Content-Encoding: UTF-8 --
"""
Pelix shell bundle.

Provides the basic command parsing and execution support to make a Pelix shell.

:author: Thomas Calmant
:copyright: Copyright 2013, isandlaTech
:license: GPLv3
:version: 0.2.3
:status: Alpha

..

    This file is part of iPOPO.

    iPOPO is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    iPOPO is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with iPOPO. If not, see <http://www.gnu.org/licenses/>.
"""

# Module version
__version_info__ = (0, 2, 3)
__version__ = ".".join(str(x) for x in __version_info__)

# Documentation strings format
__docformat__ = "restructuredtext en"

# ------------------------------------------------------------------------------

# Shell constants
from pelix.shell import SHELL_SERVICE_SPEC, SHELL_COMMAND_SPEC, \
    SHELL_UTILS_SERVICE_SPEC

# Pelix modules
from pelix.utilities import to_str, to_bytes
import pelix.constants as constants
import pelix.framework as pelix

# Standard library
import inspect
import logging
import os
import shlex
import sys
import traceback
import threading

# Before Python 3, input() was raw_input()
if sys.version_info[0] < 3:
    safe_input = raw_input

else:
    safe_input = input

# ------------------------------------------------------------------------------

DEFAULT_NAMESPACE = "default"
""" Default command name space: default """

_logger = logging.getLogger(__name__)

# ------------------------------------------------------------------------------

def _find_assignment(arg_token):
    """
    Find the first non-escaped assignment in the given argument token.
    Returns -1 if no assignment was found.

    :param arg_token: The argument token
    :return: The index of the first assignment, or -1
    """
    idx = arg_token.find('=')
    while idx != -1:
        if idx != 0:
            if arg_token[idx - 1] != '\\':
                # No escape character
                return idx

        idx = arg_token.find('=', idx + 1)

    # No assignment found
    return -1


def _make_args(args_list):
    """
    Converts the given list of arguments into a list (args) and a
    dictionary (kwargs).
    All arguments with an assignment are put into kwargs, others in args.

    :param args_list: The list of arguments to be treated
    :return: The (arg_token, kwargs) tuple.
    """
    args = []
    kwargs = {}

    for arg_token in args_list:
        idx = _find_assignment(arg_token)
        if idx != -1:
            # Assignment
            key = arg_token[:idx]
            value = arg_token[idx + 1:]
            kwargs[key] = value

        else:
            # Direct argument
            args.append(arg_token)

    return args, kwargs


def _split_ns_command(cmd_token):
    """
    Extracts the name space and the command name of the given command token.

    :param cmd_token: The command token
    :return: The extracted (name space, command) tuple
    """
    namespace = None
    cmd_split = cmd_token.split('.', 1)
    if len(cmd_split) == 1:
        # No name space given
        command = cmd_split[0]

    else:
        # Got a name space and a command
        namespace = cmd_split[0]
        command = cmd_split[1]

    if not namespace:
        # No name space given: given an empty one
        namespace = ""

    # Use lower case values only
    return namespace.lower(), command.lower()

# ------------------------------------------------------------------------------

class IOHandler(object):
    """
    Handles I/O operations between the command handler and the client
    It automatically converts the given data to bytes in Python 3.
    """
    def __init__(self, in_stream, out_stream, encoding='UTF-8'):
        """
        Sets up the printer

        :param in_stream: Input stream
        :param out_stream: Output stream
        :param encoding: Output encoding
        """
        self.input = in_stream
        self.output = out_stream
        self.encoding = encoding

        # Standard behavior
        self.flush = self.output.flush
        self.write = self.output.write

        # Specific behavior
        if sys.version_info[0] >= 3:
            if 'b' in getattr(out_stream, 'mode', ''):
                # Bytes conversion
                self.write = self._write_bytes
            else:
                # Strings accepted
                self.write = self._write_str

        # Very specific
        if in_stream is sys.stdin:
            # Warning: conflicts with the console
            self.prompt = safe_input


    def prompt(self, prompt=None):
        """
        Reads a line written by the user

        :param prompt: An optional prompt message
        :return: The read line, after a conversion to str
        """
        if prompt:
            # Print the prompt
            self.write(prompt)
            self.output.flush()

        # Read the line
        return to_str(self.input.readline())


    def _write_bytes(self, data):
        """
        Converts the given data then writes it

        :param data: Data to be written
        :return: The result of ``self.output.write()``
        """
        self.output.write(to_bytes(data, self.encoding))


    def _write_str(self, data):
        """
        Converts the given data then writes it

        :param data: Data to be written
        :return: The result of ``self.output.write()``
        """
        self.output.write(to_str(data, self.encoding))


    def write_line(self, line, *args, **kwargs):
        """
        Formats and writes a line to the output
        """
        if line is None:
            # Empty line
            self.write('\n')

        else:
            # Format the line
            line = line.format(*args, **kwargs)

            # Write it
            self.write(line)
            if line[-1] != '\n':
                # Add the trailing new line
                self.write('\n')

        self.flush()


    def write_line_no_feed(self, line, *args, **kwargs):
        """
        Formats and writes a line to the output
        """
        if line is None:
            # Empty line
            line = ""

        else:
            # Format the line
            line = line.format(*args, **kwargs)

            # Remove the trailing line feed
            if line[-1] == '\n':
                line = line[:-1]

        # Write it
        self.write(line)
        self.flush()

# ------------------------------------------------------------------------------

class ShellUtils(object):
    """
    Utility methods for the shell
    """
    def bundlestate_to_str(self, state):
        """
        Converts a bundle state integer to a string
        """
        states = {
                  pelix.Bundle.INSTALLED:   "INSTALLED",
                  pelix.Bundle.ACTIVE:      "ACTIVE",
                  pelix.Bundle.RESOLVED:    "RESOLVED",
                  pelix.Bundle.STARTING:    "STARTING",
                  pelix.Bundle.STOPPING:    "STOPPING",
                  pelix.Bundle.UNINSTALLED: "UNINSTALLED"
        }

        return states.get(state, "Unknown state (%d)".format(state))


    def make_table(self, headers, lines, prefix=None):
        """
        Generates an ASCII table according to the given headers and lines

        :param headers: List of table headers (N-tuple)
        :param lines: List of table lines (N-tuples)
        :param prefix: Optional prefix for each line
        :return: The ASCII representation of the table
        :raise ValueError: Different number of columns between headers and lines
        """
        # Normalize the prefix
        prefix = str(prefix or "")

        # Maximum lengths
        lengths = [len(title) for title in headers]

        # Store the number of columns (0-based)
        nb_columns = len(lengths) - 1

        # Lines
        str_lines = []
        for idx, line in enumerate(lines):
            # Recompute lengths
            str_line = []
            str_lines.append(str_line)
            column = -1

            try:
                for column, entry in enumerate(line):
                    str_entry = str(entry)
                    str_line.append(str_entry)

                    if len(str_entry) > lengths[column]:
                        lengths[column] = len(str_entry)

            except IndexError:
                # Line too small/big
                raise ValueError("Different sizes for header and lines "
                                 "(line {0})".format(idx + 1))

            except (TypeError, AttributeError):
                # Invalid type of line
                raise ValueError("Invalid type of line: %s",
                                 type(line).__name__)

            else:
                if column != nb_columns:
                    # Check if all lines have the same number of columns
                    raise ValueError("Different sizes for header and lines "
                                     "(line {0})".format(idx + 1))

        # Prepare the head (centered text)
        format_str = "{0}|".format(prefix)
        for column, length in enumerate(lengths):
            format_str += " {%d:^%d} |" % (column, length)

        head_str = format_str.format(*headers)

        # Prepare the separator, according the length of the headers string
        separator = '{0}{1}'.format(prefix, '-' * (len(head_str) - len(prefix)))
        idx = head_str.find('|')
        while idx != -1:
            separator = '+'.join((separator[:idx], separator[idx + 1:]))
            idx = head_str.find('|', idx + 1)

        # Prepare the output
        output = []
        output.append(separator)
        output.append(head_str)
        output.append(separator.replace('-', '='))

        # Compute the lines
        format_str = format_str.replace('^', '<')
        for line in str_lines:
            output.append(format_str.format(*line))
            output.append(separator)

        # Force the last end of line
        output.append("")

        # Join'em
        return '\n'.join(output)

# ------------------------------------------------------------------------------

class Shell(object):
    """
    A simple shell, based on shlex.

    Allows to use name spaces.
    """
    def __init__(self, context, utilities):
        """
        Sets up the shell

        :param context: The bundle context
        """
        self._commands = {}
        self._context = context
        self._utils = utilities

        # Bound services: reference -> service
        self._bound_references = {}

        # Service reference -> (name space, [commands])
        self._reference_commands = {}

        # Register basic commands
        self.register_command(None, "bd", self.bundle_details)
        self.register_command(None, "bl", self.bundles_list)

        self.register_command(None, "sd", self.service_details)
        self.register_command(None, "sl", self.services_list)

        self.register_command(None, "start", self.start)
        self.register_command(None, "stop", self.stop)
        self.register_command(None, "update", self.update)
        self.register_command(None, "install", self.install)
        self.register_command(None, "uninstall", self.uninstall)

        self.register_command(None, "properties", self.properties_list)
        self.register_command(None, "property", self.property_value)

        self.register_command(None, "sysprops", self.environment_list)
        self.register_command(None, "sysprop", self.environment_value)

        self.register_command(None, "threads", self.threads_list)
        self.register_command(None, "thread", self.thread_details)

        self.register_command(None, "help", self.print_help)
        self.register_command(None, "?", self.print_help)

        self.register_command(None, "quit", self.quit)
        self.register_command(None, "close", self.quit)
        self.register_command(None, "exit", self.quit)


    def _bind_handler(self, svc_ref):
        """
        Called if a command service has been found.
        Registers the methods of this service.

        :param svc_ref: A reference to the found service
        :return: True if the commands have been registered
        """
        if svc_ref in self._bound_references:
            # Already bound service
            return False

        # Get the service
        handler = self._context.get_service(svc_ref)

        # Get its name space
        namespace = handler.get_namespace()
        commands = []

        # Register all service methods directly
        for command, method in handler.get_methods():
            self.register_command(namespace, command, method)
            commands.append(command)

        # Store the reference
        self._bound_references[svc_ref] = handler
        self._reference_commands[svc_ref] = (namespace, commands)
        return True


    def _unbind_handler(self, svc_ref):
        """
        Called if a command service is gone.
        Unregisters its commands.

        :param svc_ref: A reference to the unbound service
        :return: True if the commands have been unregistered
        """
        if svc_ref not in self._bound_references:
            # Unknown reference
            return False

        # Unregister its commands
        namespace, commands = self._reference_commands[svc_ref]
        for command in commands:
            self.unregister(namespace, command)

        # Release the service
        self._context.unget_service(svc_ref)
        del self._bound_references[svc_ref]
        del self._reference_commands[svc_ref]
        return True


    def register_command(self, namespace, command, method):
        """
        Registers the given command to the shell.

        The namespace can be None, empty or "default"

        :param namespace: The command name space.
        :param command: The shell name of the command
        :param method: The method to call
        :return: True if the method has been registered, False if it was already
                 known or invalid
        """
        if method is None:
            _logger.error("No method given for %s.%s", namespace, command)
            return False

        # Store everything in lower case
        namespace = (namespace or "").strip().lower()
        command = (command or "").strip().lower()

        if not namespace:
            namespace = DEFAULT_NAMESPACE

        if not command:
            _logger.error("No command name given")
            return False

        if namespace not in self._commands:
            space = self._commands[namespace] = {}
        else:
            space = self._commands[namespace]

        if command in space:
            _logger.error("Command already registered: %s.%s", namespace,
                          command)
            return False

        space[command] = method
        return True


    def unregister(self, namespace, command=None):
        """
        Unregisters the given command. If command is None, the whole name space
        is unregistered.

        :param namespace: The command name space.
        :param command: The shell name of the command, or None
        :return: True if the command was known, else False
        """
        if not namespace:
            namespace = DEFAULT_NAMESPACE

        namespace = namespace.strip().lower()

        if namespace not in self._commands:
            _logger.warning("Unknown name space: %s", namespace)
            return False

        if command is not None:
            # Remove the command
            command = command.strip().lower()
            if command not in self._commands[namespace]:
                _logger.warning("Unknown command: %s.%s", namespace, command)
                return False

            del self._commands[namespace][command]

            # Remove the name space if necessary
            if not self._commands[namespace]:
                del self._commands[namespace]

        else:
            # Remove the whole name space
            del self._commands[namespace]

        return True


    def __find_command_ns(self, command):
        """
        Returns the name spaces where the given command named is registered.
        If the command exists in the default name space, the returned list will
        only contain the default name space.
        Returns an empty list of the command is unknown

        :param command: A command name
        :return: A list of name spaces
        """
        if command in self._commands[DEFAULT_NAMESPACE]:
            # Command is in the default name space, use it directly
            return [DEFAULT_NAMESPACE]

        # Look for the spaces where the command name appears
        namespaces = []
        for namespace, commands in self._commands.items():
            if command in commands:
                namespaces.append(namespace)

        namespaces.sort()
        return namespaces


    def get_ns_command(self, cmd_name):
        """
        Retrieves the name space and the command associated to the given
        command name.

        :param cmd_name: The given command name
        :return: A 2-tuple (name space, command)
        :raise ValueError: Unknown command name
        """
        namespace, command = _split_ns_command(cmd_name)
        if not namespace:
            # Name space not given, look for the command
            spaces = self.__find_command_ns(command)
            if not spaces:
                # Unknown command
                raise ValueError("Unknown command {0}".format(command))

            elif len(spaces) > 1:
                # Multiple possibilities
                raise ValueError("Multiple name spaces for {0}: {1}" \
                                 .format(command, spaces))

            else:
                # Use the found name space
                namespace = spaces[0]

        # Command found
        return namespace, command


    def execute(self, cmdline, stdin=sys.stdin, stdout=sys.stdout):
        """
        Executes the command corresponding to the given line
        """
        # Split the command line
        if not cmdline:
            return False

        # Convert the line into a string
        cmdline = to_str(cmdline)

        line_split = shlex.split(cmdline, True, True)
        if not line_split:
            return False

        # Prepare the I/O handler
        io_handler = IOHandler(stdin, stdout)

        try:
            # Extract command information
            namespace, command = self.get_ns_command(line_split[0])

        except ValueError as ex:
            # Unknown command
            io_handler.write_line(str(ex))
            return False

        # Get the content of the name space
        space = self._commands.get(namespace, None)
        if not space:
            io_handler.write_line("Unknown name space {0}", namespace)
            return False

        # Get the method object
        method = space.get(command, None)
        if method is None:
            io_handler.write_line("Unknown command: {0}.{1}", namespace,
                                  command)
            return False

        # Make arguments and keyword arguments
        args, kwargs = _make_args(line_split[1:])

        # Execute it
        try:
            result = method(io_handler, *args, **kwargs)
            # None is considered as a success
            return result is None or result

        except TypeError as ex:
            # Invalid arguments...
            _logger.exception("Error calling %s.%s: %s", namespace, command, ex)
            io_handler.write_line("Invalid method call: {0}", ex)
            return False

        except Exception as ex:
            # Error
            io_handler.write_line("{0}: {1}", type(ex).__name__, str(ex))
            return False


    def get_banner(self):
        """
        Returns the Shell banner
        """
        return "** Pelix Shell prompt **\n"


    def get_ps1(self):
        """
        Returns the PS1, the basic shell prompt
        """
        return "$ "


    def get_namespaces(self):
        """
        Retrieves the list of known name spaces (without the default one)

        :return: The list of known name spaces
        """
        namespaces = list(self._commands.keys())
        namespaces.remove(DEFAULT_NAMESPACE)
        namespaces.sort()
        return namespaces


    def get_commands(self, namespace):
        """
        Retrieves the commands of the given name space. If *namespace* is None
        or empty, it retrieves the commands of the default name space

        :param namespace: The commands name space
        :return: A list of commands names
        """
        if not namespace:
            # Default name space:
            namespace = DEFAULT_NAMESPACE

        try:
            namespace.strip().lower()
            commands = list(self._commands[namespace].keys())
            commands.sort()
            return commands

        except KeyError:
            # Unknown name space
            return []


    def bundle_details(self, io_handler, bundle_id):
        """
        Prints the details of the bundle with the given ID or name
        """
        bundle = None

        try:
            # Convert the given ID into an integer
            bundle_id = int(bundle_id)

        except ValueError:
            # Not an integer, suppose it's a bundle name
            for bundle in self._context.get_bundles():
                if bundle.get_symbolic_name() == bundle_id:
                    break

        else:
            # Integer ID: direct access
            try:
                bundle = self._context.get_bundle(bundle_id)

            except constants.BundleException:
                pass

        if bundle is None:
            # No matching bundle
            io_handler.write_line("Unknown bundle ID: {0}", bundle_id)
            return

        lines = []
        lines.append("ID      : {0}".format(bundle.get_bundle_id()))
        lines.append("Name    : {0}".format(bundle.get_symbolic_name()))
        lines.append("Version : {0}".format(bundle.get_version()))
        lines.append("State   : {0}".format(self._utils.bundlestate_to_str(
                                                        bundle.get_state())))
        lines.append("Location: {0}".format(bundle.get_location()))
        lines.append("Published services:")
        try:
            services = bundle.get_registered_services()
            if services:
                for svc_ref in services:
                    lines.append("\t{0}".format(svc_ref))

            else:
                lines.append("\tn/a")

        except constants.BundleException as ex:
            # Bundle in a invalid state
            lines.append("\tError: {0}".format(ex))

        lines.append("Services used by this bundle:")
        try:
            services = bundle.get_services_in_use()
            if services:
                for svc_ref in services:
                    lines.append("\t{0}".format(svc_ref))

            else:
                lines.append("\tn/a")

        except constants.BundleException as ex:
            # Bundle in a invalid state
            lines.append("\tError: {0}".format(ex))

        lines.append("")
        io_handler.write('\n'.join(lines))


    def bundles_list(self, io_handler):
        """
        Lists the bundles in the framework and their state
        """
        # Head of the table
        headers = ('ID', 'Name', 'State', 'Version')

        # Get the bundles
        bundles = self._context.get_bundles()

        # The framework is not in the result of get_bundles()
        bundles.insert(0, self._context.get_bundle(0))

        # Make the entries
        lines = [[str(entry) for entry in
                        (bundle.get_bundle_id(),
                         bundle.get_symbolic_name(),
                         self._utils.bundlestate_to_str(bundle.get_state()),
                         bundle.get_version())]
                 for bundle in bundles]

        # Print'em all
        io_handler.write(self._utils.make_table(headers, lines))
        io_handler.write_line("{0} bundles installed", len(lines))


    def service_details(self, io_handler, service_id):
        """
        Prints the details of the service with the given ID
        """
        svc_ref = self._context.get_service_reference(None,
                                '({0}={1})'.format(constants.SERVICE_ID,
                                                   service_id))
        if svc_ref is None:
            io_handler.write_line('Service not found: {0}', service_id)
            return

        lines = []
        lines.append("ID    : {0}".format(svc_ref.get_property(
                                            constants.SERVICE_ID)))
        lines.append("Rank  : {0}".format(svc_ref.get_property(
                                            constants.SERVICE_RANKING)))
        lines.append("Specs : {0}".format(svc_ref.get_property(
                                            constants.OBJECTCLASS)))
        lines.append("Bundle: {0}".format(svc_ref.get_bundle()))
        lines.append("Properties:")
        for key, value in svc_ref.get_properties().items():
            lines.append("\t{0} = {1}".format(key, value))

        lines.append("Bundles using this service:")
        for bundle in svc_ref.get_using_bundles():
            lines.append("\t{0}".format(bundle))

        lines.append("")
        io_handler.write('\n'.join(lines))


    def services_list(self, io_handler):
        """
        Lists the services in the framework
        """
        # Head of the table
        headers = ('ID', 'Specifications', 'Bundle', 'Ranking')

        # Lines
        references = self._context.get_all_service_references(None, None)

        # Use the reverse order (ascending service IDs instead of descending)
        references.reverse()

        # Construct the list of services
        lines = [[str(entry) for entry in
                        (ref.get_property(constants.SERVICE_ID),
                         ref.get_property(constants.OBJECTCLASS),
                         ref.get_bundle(),
                         ref.get_property(constants.SERVICE_RANKING))]
                 for ref in references]

        # Print'em all
        io_handler.write(self._utils.make_table(headers, lines))
        io_handler.write_line("{0} services registered", len(lines))


    def __extract_help(self, method):
        """
        Formats the help string for the given method

        :param method: The method to document
        :return: A tuple: (arguments list, documentation line)
        """
        if method is None:
            return "(No associated method)"

        # Get the arguments
        argspec = inspect.getargspec(method)

        # Compute the number of arguments with default value
        if argspec.defaults is not None:
            nb_optional = len(argspec.defaults)

            # Let the mandatory arguments as they are
            args = ["<{0}>".format(arg) for arg in argspec.args[2:-nb_optional]]

            # Add the other arguments
            for name, value in zip(argspec.args[-nb_optional:],
                                   argspec.defaults[-nb_optional:]):
                if value is not None:
                    args.append('[<{0}>={1}]'.format(name, value))

                else:
                    args.append('[<{0}>]'.format(name))

        else:
            # All arguments are mandatory
            args = ["<{0}>".format(arg) for arg in argspec.args[2:]]

        # Extra arguments
        if argspec.keywords:
            args.append('[<property=value> ...]')

        if argspec.varargs:
            args.append("...")

        # Get the documentation string
        doc = inspect.getdoc(method) or "(Documentation missing)"

        return ' '.join(args), ' '.join(doc.split())


    def __print_command_help(self, io_handler, namespace, cmd_name):
        """
        Prints the documentation of the given command

        :param io_handler: I/O handler
        :param namespace: Name space of the command
        :param cmd_name: Name of the command
        """
        # Extract documentation
        args, doc = self.__extract_help(self._commands[namespace][cmd_name])

        # Print the command name, and its arguments
        if args:
            io_handler.write_line("- {0} {1}", cmd_name, args)
        else:
            io_handler.write_line("- {0}", cmd_name)

        # Print the documentation line
        io_handler.write_line("\t\t{0}", doc)


    def print_help(self, io_handler, command=None):
        """
        Prints the available methods and their documentation, or the
        documentation of the given command.
        """
        if command:
            # Single command mode
            try:
                # Extract command name space and name
                namespace, cmd_name = self.get_ns_command(command)

            except ValueError as ex:
                # Unknown command
                io_handler.write_line(str(ex))
                return False

            else:
                # Print the help
                io_handler.write_line("* Name space '{0}':", namespace)
                self.__print_command_help(io_handler, namespace, cmd_name)

        else:
            # Get all name spaces
            namespaces = list(self._commands.keys())
            namespaces.remove(DEFAULT_NAMESPACE)
            namespaces.sort()
            namespaces.insert(0, DEFAULT_NAMESPACE)

            for namespace in namespaces:
                io_handler.write_line("* Name space '{0}':", namespace)

                # Get all commands in this name space
                names = [command for command in self._commands[namespace]]
                names.sort()

                for cmd_name in names:
                    self.__print_command_help(io_handler, namespace, cmd_name)


    def properties_list(self, io_handler):
        """
        Lists the properties of the framework
        """
        # Get the framework
        framework = self._context.get_bundle(0)

        # Head of the table
        headers = ('Property Name', 'Value')

        # Lines
        lines = [item for item in framework.get_properties().items()]

        # Sort lines
        lines.sort()

        # Print the table
        io_handler.write(self._utils.make_table(headers, lines))


    def property_value(self, io_handler, name):
        """
        Prints the value of the given property, looking into
        framework properties then environment variables.
        """
        io_handler.write_line(str(self._context.get_property(name)))


    def environment_list(self, io_handler):
        """
        Lists the framework process environment variables
        """
        # Head of the table
        headers = ('Environment Variable', 'Value')

        # Lines
        lines = [item for item in os.environ.items()]

        # Sort lines
        lines.sort()

        # Print the table
        io_handler.write(self._utils.make_table(headers, lines))


    def environment_value(self, io_handler, name):
        """
        Prints the value of the given environment variable
        """
        io_handler.write_line(os.getenv(name))


    def threads_list(self, io_handler):
        """
        Lists the active threads and their current code line
        """
        # Extract frames
        frames = sys._current_frames()

        # Get the thread ID -> Thread mapping
        names = threading._active.copy()

        # Sort by thread ID
        thread_ids = list(frames.keys())
        thread_ids.sort()

        lines = []
        for thread_id in thread_ids:
            # Get the corresponding stack
            stack = frames[thread_id]

            # Try to get the thread name
            try:
                name = names[thread_id].name

            except KeyError:
                name = "<unknown>"

            # Construct the code position
            lines.append('Thread ID: {0} - Name: {1}'.format(thread_id, name))
            lines.append('Line:')
            lines.extend((line.rstrip()
                          for line in traceback.format_stack(stack, 1)))
            lines.append('')

        lines.append('')

        # Sort the lines
        io_handler.write('\n'.join(lines))


    def thread_details(self, io_handler, thread_id):
        """
        Prints details about the thread with the given ID (not its name)
        """
        try:
            # Get the stack
            thread_id = int(thread_id)
            stack = sys._current_frames()[thread_id]

        except KeyError:
            io_handler.write_line("Unknown thread ID: {0}", thread_id)

        except ValueError:
            io_handler.write_line("Invalid thread ID: {0}", thread_id)

        else:
            # Get the name
            try:
                name = threading._active[thread_id].name

            except KeyError:
                name = "<unknown>"

            lines = []
            lines.append('Thread ID: {0} - Name: {1}'.format(thread_id, name))
            lines.append('Stack trace:')
            lines.extend((line.rstrip()
                          for line in traceback.format_stack(stack)))

        lines.append("")
        io_handler.write('\n'.join(lines))


    def quit(self, io_handler):
        """
        Stops the current shell session (raises a KeyboardInterrupt exception)
        """
        raise KeyboardInterrupt()


    def start(self, io_handler, bundle_id):
        """
        Starts the bundle with the given ID
        """
        bundle_id = int(bundle_id)
        bundle = self._context.get_bundle(bundle_id)
        if bundle is None:
            io_handler.write_line("Unknown bundle: {0}", bundle_id)

        bundle.start()


    def stop(self, io_handler, bundle_id):
        """
        Stops the bundle with the given ID
        """
        bundle_id = int(bundle_id)
        bundle = self._context.get_bundle(bundle_id)
        if bundle is None:
            io_handler.write_line("Unknown bundle: {0}", bundle_id)

        bundle.stop()


    def update(self, io_handler, bundle_id):
        """
        Updates the bundle with the given ID
        """
        bundle_id = int(bundle_id)
        bundle = self._context.get_bundle(bundle_id)
        if bundle is None:
            io_handler.write_line("Unknown bundle: {0}", bundle_id)

        bundle.update()


    def install(self, io_handler, module_name):
        """
        Installs the bundle with the given module name
        """
        bundle = self._context.install_bundle(module_name)
        io_handler.write_line("Bundle ID: {0}", bundle.get_bundle_id())


    def uninstall(self, io_handler, bundle_id):
        """
        Uninstalls the bundle with the given ID
        """
        bundle_id = int(bundle_id)
        bundle = self._context.get_bundle(bundle_id)
        if bundle is None:
            io_handler.write_line("Unknown bundle: {0}", bundle_id)

        bundle.uninstall()


# ------------------------------------------------------------------------------

class PelixActivator(object):
    """
    Activator class for Pelix
    """
    def __init__(self):
        """
        Sets up the activator
        """
        self._shell = None
        self._shell_reg = None
        self._utils_reg = None


    def service_changed(self, event):
        """
        Called when a command provider service event occurred
        """
        kind = event.get_kind()
        reference = event.get_service_reference()

        if kind in (pelix.ServiceEvent.REGISTERED,
                    pelix.ServiceEvent.MODIFIED):
            # New or modified service
            self._shell._bind_handler(reference)

        else:
            # Service gone or not matching anymore
            self._shell._unbind_handler(reference)


    def start(self, context):
        """
        Bundle starting

        :param context: The bundle context
        """
        try:
            # Prepare the shell utility service
            utils = ShellUtils()
            self._shell = Shell(context, utils)

            self._shell_reg = context.register_service(SHELL_SERVICE_SPEC,
                                                       self._shell, {})

            self._utils_reg = context.register_service(SHELL_UTILS_SERVICE_SPEC,
                                                       utils, {})

            # Register the service listener
            context.add_service_listener(self, None, SHELL_COMMAND_SPEC)

            # Register existing command services
            refs = context.get_all_service_references(SHELL_COMMAND_SPEC,
                                                      None)
            if refs is not None:
                for ref in refs:
                    self._shell._bind_handler(ref)

            _logger.info("Shell services registered")

        except constants.BundleException as ex:
            _logger.exception("Error registering the shell service: %s", ex)


    def stop(self, context):
        """
        Bundle stopping

        :param context: The bundle context
        """
        # Unregister the service listener
        context.remove_service_listener(self)

        # Unregister the services
        if self._shell_reg is not None:
            self._shell_reg.unregister()
            self._shell_reg = None

        if self._utils_reg is not None:
            self._utils_reg.unregister()
            self._utils_reg = None

        self._shell = None
        _logger.info("Shell services unregistered")


# Activator for Pelix
activator = PelixActivator()
