1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2007-2010 Andrew Resch <andrewresch@gmail.com>
4#
5# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with
6# the additional special exception to link portions of this program with the OpenSSL library.
7# See LICENSE for more details.
8#
9
10from __future__ import unicode_literals
11
12import logging
13import traceback
14from collections import defaultdict
15
16from six import string_types
17from twisted.internet import reactor
18from twisted.internet.defer import DeferredList, fail, maybeDeferred, succeed
19from twisted.internet.task import LoopingCall, deferLater
20
21log = logging.getLogger(__name__)
22
23
24class ComponentAlreadyRegistered(Exception):
25    pass
26
27
28class ComponentException(Exception):
29    def __init__(self, message, tb):
30        super(ComponentException, self).__init__(message)
31        self.message = message
32        self.tb = tb
33
34    def __str__(self):
35        s = super(ComponentException, self).__str__()
36        return '%s\n%s' % (s, ''.join(self.tb))
37
38    def __eq__(self, other):
39        if isinstance(other, self.__class__):
40            return self.message == other.message
41        else:
42            return False
43
44    def __ne__(self, other):
45        return not self.__eq__(other)
46
47
48class Component(object):
49    """Component objects are singletons managed by the :class:`ComponentRegistry`.
50
51    When a new Component object is instantiated, it will be automatically
52    registered with the :class:`ComponentRegistry`.
53
54    The ComponentRegistry has the ability to start, stop, pause and shutdown the
55    components registered with it.
56
57    **Events:**
58
59        **start()** - This method is called when the client has connected to a
60                  Deluge core.
61
62        **stop()** - This method is called when the client has disconnected from a
63                 Deluge core.
64
65        **update()** - This method is called every 1 second by default while the
66                   Componented is in a *Started* state.  The interval can be
67                   specified during instantiation.  The update() timer can be
68                   paused by instructing the :class:`ComponentRegistry` to pause
69                   this Component.
70
71        **shutdown()** - This method is called when the client is exiting.  If the
72                     Component is in a "Started" state when this is called, a
73                     call to stop() will be issued prior to shutdown().
74
75    **States:**
76
77        A Component can be in one of these 5 states.
78
79        **Started** - The Component has been started by the :class:`ComponentRegistry`
80                    and will have it's update timer started.
81
82        **Starting** - The Component has had it's start method called, but it hasn't
83                    fully started yet.
84
85        **Stopped** - The Component has either been stopped or has yet to be started.
86
87        **Stopping** - The Component has had it's stop method called, but it hasn't
88                    fully stopped yet.
89
90        **Paused** - The Component has had it's update timer stopped, but will
91                    still be considered in a Started state.
92
93    """
94
95    def __init__(self, name, interval=1, depend=None):
96        """Initialize component.
97
98        Args:
99            name (str): Name of component.
100            interval (int, optional): The interval in seconds to call the update function.
101            depend (list, optional): The names of components this component depends on.
102
103        """
104        self._component_name = name
105        self._component_interval = interval
106        self._component_depend = depend
107        self._component_state = 'Stopped'
108        self._component_timer = None
109        self._component_starting_deferred = None
110        self._component_stopping_deferred = None
111        _ComponentRegistry.register(self)
112
113    def __del__(self):
114        if _ComponentRegistry:
115            _ComponentRegistry.deregister(self)
116
117    def _component_start_timer(self):
118        if hasattr(self, 'update'):
119            self._component_timer = LoopingCall(self.update)
120            self._component_timer.start(self._component_interval)
121
122    def _component_start(self):
123        def on_start(result):
124            self._component_state = 'Started'
125            self._component_starting_deferred = None
126            self._component_start_timer()
127            return True
128
129        def on_start_fail(result):
130            self._component_state = 'Stopped'
131            self._component_starting_deferred = None
132            log.error(result)
133            return fail(result)
134
135        if self._component_state == 'Stopped':
136            if hasattr(self, 'start'):
137                self._component_state = 'Starting'
138                d = deferLater(reactor, 0, self.start)
139                d.addCallbacks(on_start, on_start_fail)
140                self._component_starting_deferred = d
141            else:
142                d = maybeDeferred(on_start, None)
143        elif self._component_state == 'Starting':
144            return self._component_starting_deferred
145        elif self._component_state == 'Started':
146            d = succeed(True)
147        else:
148            d = fail(
149                ComponentException(
150                    'Trying to start component "%s" but it is '
151                    'not in a stopped state. Current state: %s'
152                    % (self._component_name, self._component_state),
153                    traceback.format_stack(limit=4),
154                )
155            )
156        return d
157
158    def _component_stop(self):
159        def on_stop(result):
160            self._component_state = 'Stopped'
161            if self._component_timer and self._component_timer.running:
162                self._component_timer.stop()
163            return True
164
165        def on_stop_fail(result):
166            self._component_state = 'Started'
167            self._component_stopping_deferred = None
168            log.error(result)
169            return result
170
171        if self._component_state != 'Stopped' and self._component_state != 'Stopping':
172            if hasattr(self, 'stop'):
173                self._component_state = 'Stopping'
174                d = maybeDeferred(self.stop)
175                d.addCallback(on_stop)
176                d.addErrback(on_stop_fail)
177                self._component_stopping_deferred = d
178            else:
179                d = maybeDeferred(on_stop, None)
180
181        if self._component_state == 'Stopping':
182            return self._component_stopping_deferred
183
184        return succeed(None)
185
186    def _component_pause(self):
187        def on_pause(result):
188            self._component_state = 'Paused'
189
190        if self._component_state == 'Started':
191            if self._component_timer and self._component_timer.running:
192                d = maybeDeferred(self._component_timer.stop)
193                d.addCallback(on_pause)
194            else:
195                d = succeed(None)
196        elif self._component_state == 'Paused':
197            d = succeed(None)
198        else:
199            d = fail(
200                ComponentException(
201                    'Trying to pause component "%s" but it is '
202                    'not in a started state. Current state: %s'
203                    % (self._component_name, self._component_state),
204                    traceback.format_stack(limit=4),
205                )
206            )
207        return d
208
209    def _component_resume(self):
210        def on_resume(result):
211            self._component_state = 'Started'
212
213        if self._component_state == 'Paused':
214            d = maybeDeferred(self._component_start_timer)
215            d.addCallback(on_resume)
216        else:
217            d = fail(
218                ComponentException(
219                    'Trying to resume component "%s" but it is '
220                    'not in a paused state. Current state: %s'
221                    % (self._component_name, self._component_state),
222                    traceback.format_stack(limit=4),
223                )
224            )
225        return d
226
227    def _component_shutdown(self):
228        def on_stop(result):
229            if hasattr(self, 'shutdown'):
230                return maybeDeferred(self.shutdown)
231            return succeed(None)
232
233        d = self._component_stop()
234        d.addCallback(on_stop)
235        return d
236
237    def get_state(self):
238        return self._component_state
239
240    def start(self):
241        pass
242
243    def stop(self):
244        pass
245
246    def update(self):
247        pass
248
249    def shutdown(self):
250        pass
251
252
253class ComponentRegistry(object):
254    """The ComponentRegistry holds a list of currently registered :class:`Component` objects.
255
256    It is used to manage the Components by starting, stopping, pausing and shutting them down.
257    """
258
259    def __init__(self):
260        self.components = {}
261        # Stores all of the components that are dependent on a particular component
262        self.dependents = defaultdict(list)
263
264    def register(self, obj):
265        """Register a component object with the registry.
266
267        Note:
268            This is done automatically when a Component object is instantiated.
269
270        Args:
271            obj (Component): A component object to register.
272
273        Raises:
274            ComponentAlreadyRegistered: If a component with the same name is already registered.
275
276        """
277        name = obj._component_name
278        if name in self.components:
279            raise ComponentAlreadyRegistered(
280                'Component already registered with name %s' % name
281            )
282
283        self.components[obj._component_name] = obj
284        if obj._component_depend:
285            for depend in obj._component_depend:
286                self.dependents[depend].append(name)
287
288    def deregister(self, obj):
289        """Deregister a component from the registry.  A stop will be
290        issued to the component prior to deregistering it.
291
292        Args:
293            obj (Component): a component object to deregister
294
295        Returns:
296            Deferred: a deferred object that will fire once the Component has been sucessfully deregistered
297
298        """
299        if obj in self.components.values():
300            log.debug('Deregistering Component: %s', obj._component_name)
301            d = self.stop([obj._component_name])
302
303            def on_stop(result, name):
304                # Component may have been removed, so pop to ensure it doesn't fail
305                self.components.pop(name, None)
306
307            return d.addCallback(on_stop, obj._component_name)
308        else:
309            return succeed(None)
310
311    def start(self, names=None):
312        """Start Components, and their dependencies, that are currently in a Stopped state.
313
314        Note:
315            If no names are specified then all registered components will be started.
316
317        Args:
318            names (list): A list of Components to start and their dependencies.
319
320        Returns:
321            Deferred: Fired once all Components have been successfully started.
322
323        """
324        # Start all the components if names is empty
325        if not names:
326            names = list(self.components)
327        elif isinstance(names, string_types):
328            names = [names]
329
330        def on_depends_started(result, name):
331            return self.components[name]._component_start()
332
333        deferreds = []
334
335        for name in names:
336            if self.components[name]._component_depend:
337                # This component has depends, so we need to start them first.
338                d = self.start(self.components[name]._component_depend)
339                d.addCallback(on_depends_started, name)
340                deferreds.append(d)
341            else:
342                deferreds.append(self.components[name]._component_start())
343
344        return DeferredList(deferreds)
345
346    def stop(self, names=None):
347        """Stop Components that are currently not in a Stopped state.
348
349        Note:
350            If no names are specified then all registered components will be stopped.
351
352        Args:
353            names (list): A list of Components to stop.
354
355        Returns:
356            Deferred: Fired once all Components have been successfully stopped.
357
358        """
359        if not names:
360            names = list(self.components)
361        elif isinstance(names, string_types):
362            names = [names]
363
364        def on_dependents_stopped(result, name):
365            return self.components[name]._component_stop()
366
367        stopped_in_deferred = set()
368        deferreds = []
369
370        for name in names:
371            if name in stopped_in_deferred:
372                continue
373            if name in self.components:
374                if name in self.dependents:
375                    # If other components depend on this component, stop them first
376                    d = self.stop(self.dependents[name]).addCallback(
377                        on_dependents_stopped, name
378                    )
379                    deferreds.append(d)
380                    stopped_in_deferred.update(self.dependents[name])
381                else:
382                    deferreds.append(self.components[name]._component_stop())
383
384        return DeferredList(deferreds)
385
386    def pause(self, names=None):
387        """Pause Components that are currently in a Started state.
388
389        Note:
390            If no names are specified then all registered components will be paused.
391
392        Args:
393            names (list): A list of Components to pause.
394
395        Returns:
396            Deferred: Fired once all Components have been successfully paused.
397
398        """
399        if not names:
400            names = list(self.components)
401        elif isinstance(names, string_types):
402            names = [names]
403
404        deferreds = []
405
406        for name in names:
407            if self.components[name]._component_state == 'Started':
408                deferreds.append(self.components[name]._component_pause())
409
410        return DeferredList(deferreds)
411
412    def resume(self, names=None):
413        """Resume Components that are currently in a Paused state.
414
415        Note:
416            If no names are specified then all registered components will be resumed.
417
418        Args:
419            names (list): A list of Components to to resume.
420
421        Returns:
422            Deferred: Fired once all Components have been successfully resumed.
423
424        """
425        if not names:
426            names = list(self.components)
427        elif isinstance(names, string_types):
428            names = [names]
429
430        deferreds = []
431
432        for name in names:
433            if self.components[name]._component_state == 'Paused':
434                deferreds.append(self.components[name]._component_resume())
435
436        return DeferredList(deferreds)
437
438    def shutdown(self):
439        """Shutdown all Components regardless of state.
440
441        This will call stop() on all the components prior to shutting down. This should be called
442        when the program is exiting to ensure all Components have a chance to properly shutdown.
443
444        Returns:
445            Deferred: Fired once all Components have been successfully shut down.
446
447        """
448
449        def on_stopped(result):
450            return DeferredList(
451                [comp._component_shutdown() for comp in self.components.values()]
452            )
453
454        return self.stop(list(self.components)).addCallback(on_stopped)
455
456    def update(self):
457        """Update all Components that are in a Started state."""
458        for component in self.components.items():
459            try:
460                component.update()
461            except BaseException as ex:
462                log.exception(ex)
463
464
465_ComponentRegistry = ComponentRegistry()
466
467deregister = _ComponentRegistry.deregister
468start = _ComponentRegistry.start
469stop = _ComponentRegistry.stop
470pause = _ComponentRegistry.pause
471resume = _ComponentRegistry.resume
472update = _ComponentRegistry.update
473shutdown = _ComponentRegistry.shutdown
474
475
476def get(name):
477    """Return a reference to a component.
478
479    Args:
480        name (str): The Component name to get.
481
482    Returns:
483        Component: The Component object.
484
485    Raises:
486        KeyError: If the Component does not exist.
487
488    """
489    return _ComponentRegistry.components[name]
490