events.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. #
  2. # The Qubes OS Project, https://www.qubes-os.org/
  3. #
  4. # Copyright (C) 2014-2015 Joanna Rutkowska <joanna@invisiblethingslab.com>
  5. # Copyright (C) 2014-2015 Wojtek Porczyk <woju@invisiblethingslab.com>
  6. #
  7. # This program is free software; you can redistribute it and/or modify
  8. # it under the terms of the GNU General Public License as published by
  9. # the Free Software Foundation; either version 2 of the License, or
  10. # (at your option) any later version.
  11. #
  12. # This program is distributed in the hope that it will be useful,
  13. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15. # GNU General Public License for more details.
  16. #
  17. # You should have received a copy of the GNU General Public License along
  18. # with this program; if not, write to the Free Software Foundation, Inc.,
  19. # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
  20. #
  21. '''Qubes events.
  22. Events are fired when something happens, like VM start or stop, property change
  23. etc.
  24. '''
  25. import asyncio
  26. import collections
  27. import itertools
  28. def handler(*events):
  29. '''Event handler decorator factory.
  30. To hook an event, decorate a method in your plugin class with this
  31. decorator.
  32. Some event handlers may be defined as coroutine. In such a case, *async*
  33. should be set to :py:obj:``True``.
  34. See appropriate event documentation for details.
  35. .. note::
  36. For hooking events from extensions, see :py:func:`qubes.ext.handler`.
  37. :param str events: events
  38. '''
  39. def decorator(func):
  40. # pylint: disable=missing-docstring
  41. func.ha_events = events
  42. # mark class own handler (i.e. not from extension)
  43. func.ha_bound = True
  44. return func
  45. return decorator
  46. def ishandler(obj):
  47. '''Test if a method is hooked to an event.
  48. :param object o: suspected hook
  49. :return: :py:obj:`True` when function is a hook, :py:obj:`False` otherwise
  50. :rtype: bool
  51. '''
  52. return callable(obj) \
  53. and hasattr(obj, 'ha_events')
  54. class EmitterMeta(type):
  55. '''Metaclass for :py:class:`Emitter`'''
  56. def __init__(cls, name, bases, dict_):
  57. super(EmitterMeta, cls).__init__(name, bases, dict_)
  58. cls.__handlers__ = collections.defaultdict(set)
  59. try:
  60. propnames = set(prop.__name__ for prop in cls.property_list())
  61. except AttributeError:
  62. propnames = set()
  63. for attr in dict_:
  64. if attr in propnames:
  65. # we have to be careful, not to getattr() on properties which
  66. # may be unset
  67. continue
  68. attr = dict_[attr]
  69. if not ishandler(attr):
  70. continue
  71. for event in attr.ha_events:
  72. cls.__handlers__[event].add(attr)
  73. class Emitter(object, metaclass=EmitterMeta):
  74. '''Subject that can emit events.
  75. By default all events are disabled not to interfere with loading from XML.
  76. To enable event dispatch, set :py:attr:`events_enabled` to :py:obj:`True`.
  77. '''
  78. def __init__(self, *args, **kwargs):
  79. super(Emitter, self).__init__(*args, **kwargs)
  80. if not hasattr(self, 'events_enabled'):
  81. self.events_enabled = False
  82. self.__handlers__ = collections.defaultdict(set)
  83. def close(self):
  84. self.events_enabled = False
  85. def add_handler(self, event, func):
  86. '''Add event handler to subject's class.
  87. This is class method, it is invalid to call it on object instance.
  88. :param str event: event identificator
  89. :param collections.Callable handler: handler callable
  90. '''
  91. # pylint: disable=no-member
  92. self.__handlers__[event].add(func)
  93. def remove_handler(self, event, func):
  94. '''Remove event handler from subject's class.
  95. This is class method, it is invalid to call it on object instance.
  96. This method must be called on the same class as
  97. :py:meth:`add_handler` was called to register the handler.
  98. :param str event: event identificator
  99. :param collections.Callable handler: handler callable
  100. '''
  101. # pylint: disable=no-member
  102. self.__handlers__[event].remove(func)
  103. def _fire_event(self, event, kwargs, pre_event=False):
  104. '''Fire event for classes in given order.
  105. Do not use this method. Use :py:meth:`fire_event`.
  106. '''
  107. if not self.events_enabled:
  108. return [], []
  109. order = itertools.chain((self,), self.__class__.__mro__)
  110. if not pre_event:
  111. order = reversed(list(order))
  112. effects = []
  113. async_effects = []
  114. for i in order:
  115. try:
  116. handlers_dict = i.__handlers__
  117. except AttributeError:
  118. continue
  119. handlers = handlers_dict.get(event, set())
  120. if '*' in handlers_dict:
  121. handlers = handlers_dict['*'] | handlers
  122. for func in sorted(handlers,
  123. key=(lambda handler: hasattr(handler, 'ha_bound')),
  124. reverse=True):
  125. effect = func(self, event, **kwargs)
  126. if asyncio.iscoroutinefunction(func):
  127. async_effects.append(effect)
  128. elif effect is not None:
  129. effects.extend(effect)
  130. return effects, async_effects
  131. def fire_event(self, event, pre_event=False, **kwargs):
  132. '''Call all handlers for an event.
  133. Handlers are called for class and all parent classes, in **reversed**
  134. or **true** (depending on *pre_event* parameter)
  135. method resolution order. For each class first are called bound handlers
  136. (specified in class definition), then handlers from extensions. Aside
  137. from above, remaining order is undefined.
  138. This method call only synchronous handlers. If any asynchronous
  139. handler is registered for the event, :py:class:``RuntimeError`` is
  140. raised.
  141. .. seealso::
  142. :py:meth:`fire_event_async`
  143. :param str event: event identifier
  144. :param pre_event: is this -pre- event? reverse handlers calling order
  145. :returns: list of effects
  146. All *kwargs* are passed verbatim. They are different for different
  147. events.
  148. '''
  149. sync_effects, async_effects = self._fire_event(event, kwargs,
  150. pre_event=pre_event)
  151. if async_effects:
  152. raise RuntimeError(
  153. 'unexpected async-handler(s) {!r} for sync event {!s}'.format(
  154. async_effects, event))
  155. return sync_effects
  156. @asyncio.coroutine
  157. def fire_event_async(self, event, pre_event=False, **kwargs):
  158. '''Call all handlers for an event, allowing async calls.
  159. Handlers are called for class and all parent classes, in **reversed**
  160. or **true** (depending on *pre_event* parameter)
  161. method resolution order. For each class first are called bound handlers
  162. (specified in class definition), then handlers from extensions. Aside
  163. from above, remaining order is undefined.
  164. This method call both synchronous and asynchronous handlers. Order of
  165. asynchronous calls is, by definition, undefined.
  166. .. seealso::
  167. :py:meth:`fire_event`
  168. :param str event: event identifier
  169. :param pre_event: is this -pre- event? reverse handlers calling order
  170. :returns: list of effects
  171. All *kwargs* are passed verbatim. They are different for different
  172. events.
  173. '''
  174. sync_effects, async_effects = self._fire_event(event,
  175. kwargs, pre_event=pre_event)
  176. effects = sync_effects
  177. if async_effects:
  178. async_tasks, _ = yield from asyncio.wait(async_effects)
  179. for task in async_tasks:
  180. effect = task.result()
  181. if effect is not None:
  182. effects.extend(effect)
  183. return effects