# -*- coding: utf-8 -*-
# Elisa - Home multimedia server
# Copyright (C) 2006-2008 Fluendo Embedded S.L. (www.fluendo.com).
# All rights reserved.
#
# This file is available under one of two license agreements.
#
# This file is licensed under the GPL version 3.
# See "LICENSE.GPL" in the root of this distribution including a special
# exception to use Elisa with Fluendo's plugins.
#
# The GPL part of Elisa is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Elisa" in the root directory of this distribution package
# for details on that license.

"""
Threads monitoring service
"""

__maintainer__ = "Florian Boucault <florian@fluendo.com>"

import os, sys
import tempfile
import traceback
import threading

from elisa.base_components import service_provider

from twisted.internet import reactor

class ThreadsMonitor(service_provider.ServiceProvider):
    """
    Service checking periodically the current number of threads
    and giving a warning if a certain limit is reached.

    Warning: this component only takes into account threads created from
             Python
    """

    default_config = {'max_threads': '2',
                      'frequency': '1.0'}
    config_doc = {'max_threads': 'Maximum number of threads at any time.',
                  'frequency': 'Frequency of the monitoring in times per'
                  ' second'}

    def initialize(self):
        service_provider.ServiceProvider.initialize(self)
        self._delayed = None

    def start(self):
        self._max_threads = int(self.config.get('max_threads'))
        self._frequency = float(self.config.get('frequency'))
        self.info("Checking %s times per second that no more than %s threads \
                   are concurrently running", self._frequency, self._max_threads)

        self._delayed = reactor.callLater(1.0/self._frequency, self._check)

    def stop(self):
        if self._delayed != None and self._delayed.active():
            reactor.cancelCallLater(self._delayed)

    def _check(self):
        frames = sys._current_frames()

        # does not take into account Twisted's idle threads
        waiters_count = len(reactor.threadpool.waiters)
        threads_count = len(frames) - waiters_count

        if threads_count > self._max_threads:
            fd_log, path = tempfile.mkstemp(prefix="elisa_",
                                            suffix="_threads")

            os.write(fd_log, "Stack of concurrent Python threads\n")
            os.write(fd_log, "==================================\n\n")

            for id, frame in frames.items():
                try:
                    thread = threading._active[id]
                except KeyError:
                    # the thread died in the meantime
                    continue

                # ignore Twisted's idle threads
                if thread in reactor.threadpool.waiters:
                    continue

                os.write(fd_log, "%s\n" % thread.getName())
                stack = traceback.format_stack(frame)
                for line in stack:
                    os.write(fd_log, line)
                os.write(fd_log, "\n")

            self.warning("Too many concurrent threads (%s); stacks written at "
                         "%s" % (threads_count, path))
            os.close(fd_log)

        self._delayed = reactor.callLater(1.0/self._frequency, self._check)
