events: add support for async event handlers
See documentation for details.
This commit is contained in:
		
							parent
							
								
									6238254f49
								
							
						
					
					
						commit
						ea1a04cb19
					
				@ -81,7 +81,7 @@ class EventVisitor(ast.NodeVisitor):
 | 
				
			|||||||
            # events this way
 | 
					            # events this way
 | 
				
			||||||
            return
 | 
					            return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if name.endswith('.fire_event') or name.endswith('.fire_event_pre'):
 | 
					        if name.endswith('.fire_event') or name.endswith('.fire_event_async'):
 | 
				
			||||||
            # here we throw events; event name is the first argument; sometimes
 | 
					            # here we throw events; event name is the first argument; sometimes
 | 
				
			||||||
            # it is expressed as 'event-stem:' + some_variable
 | 
					            # it is expressed as 'event-stem:' + some_variable
 | 
				
			||||||
            eventnode = node.args[0]
 | 
					            eventnode = node.args[0]
 | 
				
			||||||
 | 
				
			|||||||
@ -143,6 +143,75 @@ returned to the caller as list. The order of this list is undefined.
 | 
				
			|||||||
   effect = o.fire_event('event1')
 | 
					   effect = o.fire_event('event1')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Asynchronous event handling
 | 
				
			||||||
 | 
					---------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Event handlers can be defined as coroutine. This way they can execute long
 | 
				
			||||||
 | 
					running actions without blocking the whole qubesd process. To define
 | 
				
			||||||
 | 
					asynchronous event handler, annotate a coroutine (a function defined with
 | 
				
			||||||
 | 
					`async def`, or decorated with `py:func:`asyncio.coroutine`) with
 | 
				
			||||||
 | 
					py:func:`qubes.events.handler` decorator. By definition, order of
 | 
				
			||||||
 | 
					such handlers is undefined.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Asynchronous events can be fired using
 | 
				
			||||||
 | 
					:py:meth:`qubes.events.Emitter.fire_event_async` method. It will handle both
 | 
				
			||||||
 | 
					synchronous and asynchronous handlers. It's an error to register asynchronous
 | 
				
			||||||
 | 
					handler (a coroutine) for synchronous event (the one fired with
 | 
				
			||||||
 | 
					:py:meth:`qubes.events.Emitter.fire_event`) - it will result in
 | 
				
			||||||
 | 
					:py:exc:`RuntimeError` exception.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. code-block:: python
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   import asyncio
 | 
				
			||||||
 | 
					   import qubes.events
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   class MyClass(qubes.events.Emitter):
 | 
				
			||||||
 | 
					       @qubes.events.handler('event1', 'event2')
 | 
				
			||||||
 | 
					       @asyncio.coroutine
 | 
				
			||||||
 | 
					       def event_handler(self, event):
 | 
				
			||||||
 | 
					           if event == 'event1':
 | 
				
			||||||
 | 
					               print('Got event 1, starting long running action')
 | 
				
			||||||
 | 
					               yield from asyncio.sleep(10)
 | 
				
			||||||
 | 
					               print('Done')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   o = MyClass()
 | 
				
			||||||
 | 
					   loop = asyncio.get_event_loop()
 | 
				
			||||||
 | 
					   loop.run_until_complete(o.fire_event_async('event1'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Asynchronous event handlers can also return value - but only a collection, not
 | 
				
			||||||
 | 
					yield individual values (because of python limitation):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. code-block:: python
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   import asyncio
 | 
				
			||||||
 | 
					   import qubes.events
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   class MyClass(qubes.events.Emitter):
 | 
				
			||||||
 | 
					       @qubes.events.handler('event1')
 | 
				
			||||||
 | 
					       @asyncio.coroutine
 | 
				
			||||||
 | 
					       def event_handler(self, event):
 | 
				
			||||||
 | 
					            print('Got event, starting long running action')
 | 
				
			||||||
 | 
					            yield from asyncio.sleep(10)
 | 
				
			||||||
 | 
					            return ('result1', 'result2')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       @qubes.events.handler('event1')
 | 
				
			||||||
 | 
					       @asyncio.coroutine
 | 
				
			||||||
 | 
					       def another_handler(self, event):
 | 
				
			||||||
 | 
					            print('Got event, starting long running action')
 | 
				
			||||||
 | 
					            yield from asyncio.sleep(10)
 | 
				
			||||||
 | 
					            return ('result3', 'result4')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					       @qubes.events.handler('event1')
 | 
				
			||||||
 | 
					       def synchronous_handler(self, event):
 | 
				
			||||||
 | 
					            yield 'sync result'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					   o = MyClass()
 | 
				
			||||||
 | 
					   loop = asyncio.get_event_loop()
 | 
				
			||||||
 | 
					   # returns ['sync result', 'result1', 'result2', 'result3', 'result4'],
 | 
				
			||||||
 | 
					   # possibly not in order
 | 
				
			||||||
 | 
					   effects = loop.run_until_complete(o.fire_event_async('event1'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Module contents
 | 
					Module contents
 | 
				
			||||||
---------------
 | 
					---------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -24,7 +24,7 @@
 | 
				
			|||||||
Events are fired when something happens, like VM start or stop, property change
 | 
					Events are fired when something happens, like VM start or stop, property change
 | 
				
			||||||
etc.
 | 
					etc.
 | 
				
			||||||
'''
 | 
					'''
 | 
				
			||||||
 | 
					import asyncio
 | 
				
			||||||
import collections
 | 
					import collections
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import itertools
 | 
					import itertools
 | 
				
			||||||
@ -36,14 +36,14 @@ def handler(*events):
 | 
				
			|||||||
    To hook an event, decorate a method in your plugin class with this
 | 
					    To hook an event, decorate a method in your plugin class with this
 | 
				
			||||||
    decorator.
 | 
					    decorator.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    It probably makes no sense to specify more than one handler for specific
 | 
					    Some event handlers may be defined as coroutine. In such a case, *async*
 | 
				
			||||||
    event in one class, because handlers are not run concurrently and there is
 | 
					    should be set to :py:obj:``True``.
 | 
				
			||||||
    no guarantee of the order of execution.
 | 
					    See appropriate event documentation for details.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .. note::
 | 
					    .. note::
 | 
				
			||||||
        For hooking events from extensions, see :py:func:`qubes.ext.handler`.
 | 
					        For hooking events from extensions, see :py:func:`qubes.ext.handler`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :param str event: event type
 | 
					    :param str events: events
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def decorator(func):
 | 
					    def decorator(func):
 | 
				
			||||||
@ -141,13 +141,14 @@ class Emitter(object, metaclass=EmitterMeta):
 | 
				
			|||||||
        '''
 | 
					        '''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if not self.events_enabled:
 | 
					        if not self.events_enabled:
 | 
				
			||||||
            return []
 | 
					            return [], []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        order = itertools.chain((self,), self.__class__.__mro__)
 | 
					        order = itertools.chain((self,), self.__class__.__mro__)
 | 
				
			||||||
        if not pre_event:
 | 
					        if not pre_event:
 | 
				
			||||||
            order = reversed(list(order))
 | 
					            order = reversed(list(order))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        effects = []
 | 
					        effects = []
 | 
				
			||||||
 | 
					        async_effects = []
 | 
				
			||||||
        for i in order:
 | 
					        for i in order:
 | 
				
			||||||
            try:
 | 
					            try:
 | 
				
			||||||
                handlers_dict = i.__handlers__
 | 
					                handlers_dict = i.__handlers__
 | 
				
			||||||
@ -160,9 +161,11 @@ class Emitter(object, metaclass=EmitterMeta):
 | 
				
			|||||||
                    key=(lambda handler: hasattr(handler, 'ha_bound')),
 | 
					                    key=(lambda handler: hasattr(handler, 'ha_bound')),
 | 
				
			||||||
                    reverse=True):
 | 
					                    reverse=True):
 | 
				
			||||||
                effect = func(self, event, **kwargs)
 | 
					                effect = func(self, event, **kwargs)
 | 
				
			||||||
                if effect is not None:
 | 
					                if asyncio.iscoroutinefunction(func):
 | 
				
			||||||
 | 
					                    async_effects.append(effect)
 | 
				
			||||||
 | 
					                elif effect is not None:
 | 
				
			||||||
                    effects.extend(effect)
 | 
					                    effects.extend(effect)
 | 
				
			||||||
        return effects
 | 
					        return effects, async_effects
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def fire_event(self, event, pre_event=False, **kwargs):
 | 
					    def fire_event(self, event, pre_event=False, **kwargs):
 | 
				
			||||||
        '''Call all handlers for an event.
 | 
					        '''Call all handlers for an event.
 | 
				
			||||||
@ -173,6 +176,13 @@ class Emitter(object, metaclass=EmitterMeta):
 | 
				
			|||||||
        (specified in class definition), then handlers from extensions. Aside
 | 
					        (specified in class definition), then handlers from extensions. Aside
 | 
				
			||||||
        from above, remaining order is undefined.
 | 
					        from above, remaining order is undefined.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This method call only synchronous handlers. If any asynchronous
 | 
				
			||||||
 | 
					        handler is registered for the event, :py:class:``RuntimeError`` is
 | 
				
			||||||
 | 
					        raised.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .. seealso::
 | 
				
			||||||
 | 
					            :py:meth:`fire_event_async`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param str event: event identifier
 | 
					        :param str event: event identifier
 | 
				
			||||||
        :param pre_event: is this -pre- event? reverse handlers calling order
 | 
					        :param pre_event: is this -pre- event? reverse handlers calling order
 | 
				
			||||||
        :returns: list of effects
 | 
					        :returns: list of effects
 | 
				
			||||||
@ -181,4 +191,46 @@ class Emitter(object, metaclass=EmitterMeta):
 | 
				
			|||||||
        events.
 | 
					        events.
 | 
				
			||||||
        '''
 | 
					        '''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return self._fire_event(event, kwargs, pre_event=pre_event)
 | 
					        sync_effects, async_effects = self._fire_event(event, kwargs,
 | 
				
			||||||
 | 
					            pre_event=pre_event)
 | 
				
			||||||
 | 
					        if async_effects:
 | 
				
			||||||
 | 
					            raise RuntimeError(
 | 
				
			||||||
 | 
					                'unexpected async-handler(s) {!r} for sync event {!s}'.format(
 | 
				
			||||||
 | 
					                    async_effects, event))
 | 
				
			||||||
 | 
					        return sync_effects
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @asyncio.coroutine
 | 
				
			||||||
 | 
					    def fire_event_async(self, event, pre_event=False, **kwargs):
 | 
				
			||||||
 | 
					        '''Call all handlers for an event, allowing async calls.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Handlers are called for class and all parent classes, in **reversed**
 | 
				
			||||||
 | 
					        or **true** (depending on *pre_event* parameter)
 | 
				
			||||||
 | 
					        method resolution order. For each class first are called bound handlers
 | 
				
			||||||
 | 
					        (specified in class definition), then handlers from extensions. Aside
 | 
				
			||||||
 | 
					        from above, remaining order is undefined.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        This method call both synchronous and asynchronous handlers. Order of
 | 
				
			||||||
 | 
					        asynchronous calls is, by definition, undefined.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        .. seealso::
 | 
				
			||||||
 | 
					            :py:meth:`fire_event`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        :param str event: event identifier
 | 
				
			||||||
 | 
					        :param pre_event: is this -pre- event? reverse handlers calling order
 | 
				
			||||||
 | 
					        :returns: list of effects
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        All *kwargs* are passed verbatim. They are different for different
 | 
				
			||||||
 | 
					        events.
 | 
				
			||||||
 | 
					        '''
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sync_effects, async_effects = self._fire_event(event,
 | 
				
			||||||
 | 
					            kwargs, pre_event=pre_event)
 | 
				
			||||||
 | 
					        effects = sync_effects
 | 
				
			||||||
 | 
					        if async_effects:
 | 
				
			||||||
 | 
					            async_tasks, _ = yield from asyncio.wait(async_effects)
 | 
				
			||||||
 | 
					            for task in async_tasks:
 | 
				
			||||||
 | 
					                effect = task.result()
 | 
				
			||||||
 | 
					                if effect is not None:
 | 
				
			||||||
 | 
					                    effects.extend(effect)
 | 
				
			||||||
 | 
					        return effects
 | 
				
			||||||
 | 
				
			|||||||
@ -159,6 +159,18 @@ class TestEmitter(qubes.events.Emitter):
 | 
				
			|||||||
        self.fired_events[(event, ev_kwargs)] += 1
 | 
					        self.fired_events[(event, ev_kwargs)] += 1
 | 
				
			||||||
        return effects
 | 
					        return effects
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @asyncio.coroutine
 | 
				
			||||||
 | 
					    def fire_event_async(self, event, pre_event=False, **kwargs):
 | 
				
			||||||
 | 
					        effects = yield from super(TestEmitter, self).fire_event_async(
 | 
				
			||||||
 | 
					            event, pre_event=pre_event, **kwargs)
 | 
				
			||||||
 | 
					        ev_kwargs = frozenset(
 | 
				
			||||||
 | 
					            (key,
 | 
				
			||||||
 | 
					                frozenset(value.items()) if isinstance(value, dict) else value)
 | 
				
			||||||
 | 
					            for key, value in kwargs.items()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        self.fired_events[(event, ev_kwargs)] += 1
 | 
				
			||||||
 | 
					        return effects
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def expectedFailureIfTemplate(templates):
 | 
					def expectedFailureIfTemplate(templates):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
 | 
				
			|||||||
@ -18,6 +18,7 @@
 | 
				
			|||||||
# with this program; if not, write to the Free Software Foundation, Inc.,
 | 
					# with this program; if not, write to the Free Software Foundation, Inc.,
 | 
				
			||||||
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 | 
					# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 | 
				
			||||||
#
 | 
					#
 | 
				
			||||||
 | 
					import asyncio
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import qubes.events
 | 
					import qubes.events
 | 
				
			||||||
import qubes.tests
 | 
					import qubes.tests
 | 
				
			||||||
@ -134,3 +135,36 @@ class TC_00_Emitter(qubes.tests.QubesTestCase):
 | 
				
			|||||||
                ['testevent_2', 'testevent_1'])
 | 
					                ['testevent_2', 'testevent_1'])
 | 
				
			||||||
            self.assertEqual(list(effect2),
 | 
					            self.assertEqual(list(effect2),
 | 
				
			||||||
                ['testevent_1'])
 | 
					                ['testevent_1'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def test_005_fire_for_effect_async(self):
 | 
				
			||||||
 | 
					        class TestEmitter(qubes.events.Emitter):
 | 
				
			||||||
 | 
					            @qubes.events.handler('testevent')
 | 
				
			||||||
 | 
					            @asyncio.coroutine
 | 
				
			||||||
 | 
					            def on_testevent_1(self, event):
 | 
				
			||||||
 | 
					                pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            @qubes.events.handler('testevent')
 | 
				
			||||||
 | 
					            @asyncio.coroutine
 | 
				
			||||||
 | 
					            def on_testevent_2(self, event):
 | 
				
			||||||
 | 
					                yield from asyncio.sleep(0.01)
 | 
				
			||||||
 | 
					                return ['testvalue1']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            @qubes.events.handler('testevent')
 | 
				
			||||||
 | 
					            @asyncio.coroutine
 | 
				
			||||||
 | 
					            def on_testevent_3(self, event):
 | 
				
			||||||
 | 
					                return ('testvalue2', 'testvalue3')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            @qubes.events.handler('testevent')
 | 
				
			||||||
 | 
					            def on_testevent_4(self, event):
 | 
				
			||||||
 | 
					                return ('testvalue4',)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        loop = asyncio.get_event_loop()
 | 
				
			||||||
 | 
					        emitter = TestEmitter()
 | 
				
			||||||
 | 
					        emitter.events_enabled = True
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        effect = loop.run_until_complete(emitter.fire_event_async('testevent'))
 | 
				
			||||||
 | 
					        loop.close()
 | 
				
			||||||
 | 
					        asyncio.set_event_loop(None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.assertCountEqual(effect,
 | 
				
			||||||
 | 
					            ('testvalue1', 'testvalue2', 'testvalue3', 'testvalue4'))
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user